├── .editorconfig ├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .stylelintrc.js ├── README.md ├── antd-theme └── theme.less ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── lang ├── cn.json ├── en.json └── index.js ├── package.json ├── public ├── charting_library │ ├── charting_library.min.d.ts │ ├── charting_library.min.js │ ├── datafeed-api.d.ts │ └── static │ │ ├── ar-tv-chart.4983fae3fbe165749507.html │ │ ├── bundles │ │ ├── 10.5fc4bebecf627d0a0f2f.js │ │ ├── 13.44339b003e12eadcd24e.js │ │ ├── 15.14e44c9fe762e0c75635.js │ │ ├── 9.8309436d1561a99a7fad.js │ │ ├── crosshair.6c091f7d5427d0c5e6d9dc3a90eb2b20.cur │ │ ├── dot.ed68e83c16f77203e73dbc4c3a7c7fa1.cur │ │ ├── ds-property-pages.bfd2564e4fd770bc0b0c.js │ │ ├── editobjectdialog.c39226f8a9a3231aef9a.js │ │ ├── eraser.0579d40b812fa2c3ffe72e5803a6e14c.cur │ │ ├── go-to-date-dialog-impl.c121ad9ddaf13331c500.js │ │ ├── grab.bc156522a6b55a60be9fae15c14b66c5.cur │ │ ├── grabbing.1c0862a8a8c0fb02885557bc97fdafe7.cur │ │ ├── ie-fallback-logos.8319ee6d7ee230348d2d.js │ │ ├── lazy-jquery-ui.e4174a65a8360a06f2da.js │ │ ├── lazy-velocity.832705322dfa540785f6.js │ │ ├── library.5dad2b9a34c29a058bba.js │ │ ├── library.d8f5cc7dfd69730985ef782d21a1321f.css │ │ ├── lt-pane-views.7df9edb244fbdda2ee13.js │ │ ├── objecttreedialog.33adfe386aa3612bb60e.js │ │ ├── propertypagesfactory.37bd38e8744b0cc04a07.js │ │ ├── symbol-info-dialog-impl.eba97409764f2b04ac83.js │ │ ├── take-chart-image-dialog-impl.b60665314521ef3361a1.js │ │ ├── vendors.a94ef44ed5c201cefcf6ad7460788c1a.css │ │ ├── vendors.f495e82849a430ceb659.js │ │ └── zoom.e21f24dd632c7069139bc47ae89c54b5.cur │ │ ├── cs-tv-chart.4983fae3fbe165749507.html │ │ ├── da_DK-tv-chart.4983fae3fbe165749507.html │ │ ├── de-tv-chart.4983fae3fbe165749507.html │ │ ├── el-tv-chart.4983fae3fbe165749507.html │ │ ├── en-tv-chart.4983fae3fbe165749507.html │ │ ├── es-tv-chart.4983fae3fbe165749507.html │ │ ├── et_EE-tv-chart.4983fae3fbe165749507.html │ │ ├── fa-tv-chart.4983fae3fbe165749507.html │ │ ├── fonts │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ │ ├── fr-tv-chart.4983fae3fbe165749507.html │ │ ├── he_IL-tv-chart.4983fae3fbe165749507.html │ │ ├── hu_HU-tv-chart.4983fae3fbe165749507.html │ │ ├── id_ID-tv-chart.4983fae3fbe165749507.html │ │ ├── images │ │ ├── balloon.png │ │ ├── bar-loader.gif │ │ ├── button-bg.png │ │ ├── charting_library │ │ │ ├── logo-widget-copyright-faded.png │ │ │ └── logo-widget-copyright.png │ │ ├── controlll.png │ │ ├── delayed.png │ │ ├── dialogs │ │ │ ├── checkbox.png │ │ │ ├── close-flat.png │ │ │ ├── large-slider-handle.png │ │ │ ├── linewidth-slider.png │ │ │ └── opacity-slider.png │ │ ├── icons.png │ │ ├── prediction-clock-black.png │ │ ├── prediction-clock-white.png │ │ ├── prediction-failure-white.png │ │ ├── prediction-success-white.png │ │ ├── select-bg.png │ │ ├── sidetoolbar │ │ │ ├── instruments.png │ │ │ └── toolgroup.png │ │ ├── svg │ │ │ ├── chart │ │ │ │ ├── bucket2.svg │ │ │ │ ├── font.svg │ │ │ │ ├── large-slider-handle.svg │ │ │ │ └── pencil2.svg │ │ │ └── question-mark-rounded.svg │ │ ├── tvcolorpicker-bg-gradient.png │ │ ├── tvcolorpicker-bg.png │ │ ├── tvcolorpicker-check.png │ │ ├── tvcolorpicker-sprite.png │ │ └── warning-icon.png │ │ ├── it-tv-chart.4983fae3fbe165749507.html │ │ ├── ja-tv-chart.4983fae3fbe165749507.html │ │ ├── ko-tv-chart.4983fae3fbe165749507.html │ │ ├── lib │ │ └── external │ │ │ └── spin.min.js │ │ ├── ms_MY-tv-chart.4983fae3fbe165749507.html │ │ ├── nl_NL-tv-chart.4983fae3fbe165749507.html │ │ ├── no-tv-chart.4983fae3fbe165749507.html │ │ ├── pl-tv-chart.4983fae3fbe165749507.html │ │ ├── pt-tv-chart.4983fae3fbe165749507.html │ │ ├── ro-tv-chart.4983fae3fbe165749507.html │ │ ├── ru-tv-chart.4983fae3fbe165749507.html │ │ ├── sk_SK-tv-chart.4983fae3fbe165749507.html │ │ ├── sv-tv-chart.4983fae3fbe165749507.html │ │ ├── th-tv-chart.4983fae3fbe165749507.html │ │ ├── tr-tv-chart.4983fae3fbe165749507.html │ │ ├── vi-tv-chart.4983fae3fbe165749507.html │ │ ├── zh-tv-chart.4983fae3fbe165749507.html │ │ └── zh_TW-tv-chart.4983fae3fbe165749507.html ├── datafeeds │ ├── README.md │ └── udf │ │ ├── .npmrc │ │ ├── README.md │ │ ├── dist │ │ ├── bundle.js │ │ └── polyfills.js │ │ ├── lib │ │ ├── data-pulse-provider.js │ │ ├── helpers.js │ │ ├── history-provider.js │ │ ├── iquotes-provider.js │ │ ├── quotes-provider.js │ │ ├── quotes-pulse-provider.js │ │ ├── requester.js │ │ ├── symbols-storage.js │ │ ├── udf-compatible-datafeed-base.js │ │ └── udf-compatible-datafeed.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src │ │ ├── data-pulse-provider.ts │ │ ├── helpers.ts │ │ ├── history-provider.ts │ │ ├── iquotes-provider.ts │ │ ├── polyfills.es6 │ │ ├── quotes-provider.ts │ │ ├── quotes-pulse-provider.ts │ │ ├── requester.ts │ │ ├── symbols-storage.ts │ │ ├── udf-compatible-datafeed-base.ts │ │ └── udf-compatible-datafeed.ts │ │ └── tsconfig.json ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── server ├── config.js ├── controller │ ├── kline.js │ └── user.js ├── index.js ├── mock │ ├── index.js │ ├── kline.js │ └── lib │ │ └── utils.js ├── router │ ├── exchange.js │ ├── index.js │ └── user.js └── socket.js ├── src ├── actions │ ├── exchangeActions.js │ ├── globalActions.js │ ├── index.js │ └── userActions.js ├── components │ └── Layout │ │ ├── LayoutView.js │ │ ├── LayoutView.scss │ │ └── index.js ├── history.js ├── index.js ├── pages │ └── Exchange │ │ ├── Deals │ │ ├── DealsView.js │ │ ├── DealsView.scss │ │ └── index.js │ │ ├── Depth │ │ ├── DepthView.js │ │ ├── DepthView.scss │ │ └── index.js │ │ ├── Footer │ │ ├── Footer.scss │ │ └── index.js │ │ ├── Grid │ │ ├── GridView.js │ │ └── GridView.scss │ │ ├── Header │ │ ├── Header.scss │ │ └── index.js │ │ ├── KLine │ │ ├── KLineView.js │ │ ├── KLineView.scss │ │ └── index.js │ │ ├── Markets │ │ ├── List.js │ │ ├── MarketsView.js │ │ ├── MarketsView.scss │ │ └── index.js │ │ ├── Mscy │ │ ├── Mscy.js │ │ ├── Mscy.scss │ │ └── package.json │ │ ├── Notice │ │ ├── Notice.js │ │ ├── Notice.scss │ │ └── package.json │ │ ├── Orders │ │ ├── OrdersView.js │ │ ├── OrdersView.scss │ │ └── index.js │ │ ├── TradePanel │ │ ├── Limit.js │ │ ├── Market.js │ │ ├── TradePanel.scss │ │ └── index.js │ │ ├── assets │ │ └── logo.svg │ │ ├── common.scss │ │ └── index.js ├── reducers │ ├── exchangeReducer.js │ ├── index.js │ └── userReducer.js ├── registerServiceWorker.js ├── sagas │ ├── exchangeActions.js │ ├── index.js │ └── userActions.js ├── services │ ├── connection.js │ ├── constants.js │ ├── language.js │ └── lib │ │ └── re-websocket.js ├── store.js └── utils │ ├── common.js │ └── converter.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | # editorconfig-tools is unable to ignore longs strings or urls 20 | max_line_length = null 21 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=true 2 | REACT_APP_API=http://localhost:8000/api 3 | REACT_APP_WS=ws://localhost:8000/ws 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "react-app", 11 | 'plugin:css-modules/recommended', 12 | 'prettier', 13 | 'prettier/flowtype', 14 | 'prettier/react', 15 | ], 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "experimentalObjectRestSpread": true, 19 | "jsx": true 20 | }, 21 | "sourceType": "module" 22 | }, 23 | "plugins": [ 24 | "react", 'css-modules', 'prettier' 25 | ], 26 | "rules": { 27 | "css-modules/no-unused-class": [1, { "camelCase": true }], 28 | "css-modules/no-undef-class": [1, { "camelCase": true }], 29 | 'prettier/prettier': 'error', 30 | 'no-console': [ 31 | 0, 32 | { 33 | allow: ['warn', 'error', 'info'], 34 | }, 35 | ], 36 | 'no-undef': 0, 37 | 'jsx-a11y/href-no-hash': 'off', 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /.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 | # webstorm 13 | .idea 14 | 15 | # node-sass-chokidar 16 | src/**/*.css 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // Prettier configuration 2 | // https://prettier.io/docs/en/configuration.html 3 | module.exports = { 4 | semi: true, 5 | printWidth: 80, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | }; 9 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "stylelint-config-standard", 3 | "plugins": [ 4 | "stylelint-order" 5 | ], 6 | "rules": { 7 | "string-quotes": "single", 8 | "number-leading-zero": "never", 9 | "property-no-unknown": [ 10 | true, 11 | { 12 | "ignoreProperties": [ 13 | "composes" 14 | ] 15 | } 16 | ], 17 | "selector-pseudo-class-no-unknown": [ 18 | true, 19 | { 20 | "ignorePseudoClasses": [ 21 | "global" 22 | ] 23 | } 24 | ], 25 | "order/order": [ 26 | "declarations", 27 | "custom-properties", 28 | "dollar-variables", 29 | "rules", 30 | "at-rules" 31 | ], 32 | "order/properties-order": [ 33 | "position", 34 | "top", 35 | "right", 36 | "bottom", 37 | "left", 38 | "float", 39 | "clear", 40 | "display", 41 | "flex", 42 | "flex-grow", 43 | "flex-shrink", 44 | "flex-basis", 45 | "flex-flow", 46 | "flex-direction", 47 | "flex-wrap", 48 | "justify-content", 49 | "align-content", 50 | "align-items", 51 | "align-self", 52 | "order", 53 | "grid", 54 | "grid-template-rows", 55 | "grid-template-columns", 56 | "grid-template-areas", 57 | "grid-auto-rows", 58 | "grid-auto-columns", 59 | "grid-auto-flow", 60 | "grid-column-gap", 61 | "grid-row-gap", 62 | "grid-template", 63 | "grid-template-rows", 64 | "grid-template-columns", 65 | "grid-template-areas", 66 | "grid-gap", 67 | "grid-row-gap", 68 | "grid-column-gap", 69 | "grid-area", 70 | "grid-row-start", 71 | "grid-row-end", 72 | "grid-column-start", 73 | "grid-column-end", 74 | "grid-column", 75 | "grid-column-start", 76 | "grid-column-end", 77 | "grid-row", 78 | "grid-row-start", 79 | "grid-row-end", 80 | "table-layout", 81 | "empty-cells", 82 | "caption-side", 83 | "border-collapse", 84 | "border-spacing", 85 | "list-style", 86 | "list-style-type", 87 | "list-style-position", 88 | "list-style-image", 89 | "ruby-align", 90 | "ruby-merge", 91 | "ruby-position", 92 | "box-sizing", 93 | "width", 94 | "min-width", 95 | "max-width", 96 | "height", 97 | "min-height", 98 | "max-height", 99 | "padding", 100 | "padding-top", 101 | "padding-right", 102 | "padding-bottom", 103 | "padding-left", 104 | "border", 105 | "border-width", 106 | "border-top-width", 107 | "border-right-width", 108 | "border-bottom-width", 109 | "border-left-width", 110 | "border-style", 111 | "border-top-style", 112 | "border-right-style", 113 | "border-bottom-style", 114 | "border-left-style", 115 | "border-color", 116 | "border-top-color", 117 | "border-right-color", 118 | "border-bottom-color", 119 | "border-left-color", 120 | "border-image", 121 | "border-image-source", 122 | "border-image-slice", 123 | "border-image-width", 124 | "border-image-outset", 125 | "border-image-repeat", 126 | "border-top", 127 | "border-top-width", 128 | "border-top-style", 129 | "border-top-color", 130 | "border-top", 131 | "border-right-width", 132 | "border-right-style", 133 | "border-right-color", 134 | "border-bottom", 135 | "border-bottom-width", 136 | "border-bottom-style", 137 | "border-bottom-color", 138 | "border-left", 139 | "border-left-width", 140 | "border-left-style", 141 | "border-left-color", 142 | "border-radius", 143 | "border-top-right-radius", 144 | "border-bottom-right-radius", 145 | "border-bottom-left-radius", 146 | "border-top-left-radius", 147 | "outline", 148 | "outline-width", 149 | "outline-color", 150 | "outline-style", 151 | "outline-offset", 152 | "margin", 153 | "margin-top", 154 | "margin-right", 155 | "margin-bottom", 156 | "margin-left", 157 | "color", 158 | "background", 159 | "background-image", 160 | "background-position", 161 | "background-size", 162 | "background-repeat", 163 | "background-origin", 164 | "background-clip", 165 | "background-attachment", 166 | "background-color", 167 | "background-blend-mode", 168 | "isolation", 169 | "clip-path", 170 | "mask", 171 | "mask-image", 172 | "mask-mode", 173 | "mask-position", 174 | "mask-size", 175 | "mask-repeat", 176 | "mask-origin", 177 | "mask-clip", 178 | "mask-composite", 179 | "mask-type", 180 | "filter", 181 | "box-shadow", 182 | "opacity", 183 | "visibility", 184 | "overflow", 185 | "overflow-x", 186 | "overflow-y", 187 | "vertical-align", 188 | "columns", 189 | "columns-width", 190 | "columns-count", 191 | "column-rule", 192 | "column-rule-width", 193 | "column-rule-style", 194 | "column-rule-color", 195 | "column-fill", 196 | "column-span", 197 | "column-gap", 198 | "orphans", 199 | "writing-mode", 200 | "text-combine-upright", 201 | "unicode-bidi", 202 | "text-orientation", 203 | "direction", 204 | "text-rendering", 205 | "font-feature-settings", 206 | "font-language-override", 207 | "font", 208 | "font-style", 209 | "font-variant", 210 | "font-weight", 211 | "font-stretch", 212 | "font-size", 213 | "font-family", 214 | "line-height", 215 | "text-overflow", 216 | "white-space", 217 | "overflow-wrap", 218 | "word-wrap", 219 | "word-break", 220 | "line-break", 221 | "hyphens", 222 | "text-align", 223 | "text-align-last", 224 | "text-justify", 225 | "font-synthesis", 226 | "font-size-adjust", 227 | "letter-spacing", 228 | "font-kerning", 229 | "word-spacing", 230 | "text-transform", 231 | "quotes", 232 | "tab-size", 233 | "text-indent", 234 | "text-emphasis", 235 | "text-emphasis-style", 236 | "text-emphasis-color", 237 | "text-emphasis-position", 238 | "text-decoration", 239 | "text-decoration-color", 240 | "text-decoration-style", 241 | "text-decoration-line", 242 | "text-underline-position", 243 | "text-shadow", 244 | "image-rendering", 245 | "image-orientation", 246 | "image-resolution", 247 | "shape-image-threshold", 248 | "shape-outside", 249 | "shape-margin", 250 | "transform-style", 251 | "transform", 252 | "transform-box", 253 | "transform-origin", 254 | "perspective", 255 | "perspective-origin", 256 | "backface-visibility", 257 | "transition", 258 | "transition-property", 259 | "transition-duration", 260 | "transition-timing-function", 261 | "transition-delay", 262 | "animation", 263 | "animation-name", 264 | "animation-duration", 265 | "animation-timing-function", 266 | "animation-delay", 267 | "animation-iteration-count", 268 | "animation-direction", 269 | "animation-fill-mode", 270 | "animation-play-state", 271 | "scroll-behavior", 272 | "scroll-snap-type", 273 | "scroll-snap-destination", 274 | "scroll-snap-coordinate", 275 | "resize", 276 | "cursor", 277 | "touch-action", 278 | "caret-color", 279 | "ime-mode", 280 | "object-fit", 281 | "object-position", 282 | "content", 283 | "counter-reset", 284 | "counter-increment", 285 | "will-change", 286 | "pointer-events", 287 | "z-index", 288 | "all", 289 | "page-break-before", 290 | "page-break-after", 291 | "page-break-inside", 292 | "widows" 293 | ], 294 | } 295 | }; 296 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 对不起大伙,这个区块链前端项目我停止更新了。sorry sorry sorry。 2 | 3 | 仿火币交易所,要做的东西比较多,一点点更新。最终要实现typescript、服务端渲染、完整流程的交易所。 4 | 5 | ## 预置命令 6 | 7 | 安装 8 | ```shell 9 | $ yarn install 10 | ``` 11 | 12 | 运行 13 | ```shell 14 | $ yarn start 15 | ``` 16 | 17 | 带调试(sourcemap和css原命名)打包 18 | ```shell 19 | $ yarn build 20 | ``` 21 | 22 | 启动伪服务端(提供http和ws测试数据) 23 | ```shell 24 | $ yarn fake-server 25 | ``` 26 | 27 | 产品打包 28 | ```shell 29 | $ yarn build:release 30 | ``` 31 | 32 | ### 订阅 33 | 34 | ***客户端*** 35 | 成功建立和 WebSocket API 的连接之后,向 Server 发送如下格式的数据来订阅数据: 36 | ```js 37 | { 38 | "sub": "market.$symbol.kline.$period", 39 | "id": "id generate by client" 40 | } 41 | ``` 42 | 43 | - sub 订阅 44 | - id 客户端自定义 45 | 46 | 参数名称 | 描述 | 取值 47 | ---|---|--- 48 | symbol | 交易对 | ethbtc, ltcbtc, etcbtc, bchbtc...... 49 | period | K线周期 | 1min, 5min, 15min, 30min, 60min, 1day, 1mon, 1week, 1year 50 | 51 | ***服务端*** 52 | 返回订阅成功消息 53 | ```js 54 | { 55 | "id":"", 56 | "status":"ok", 57 | "subbed":"market.$symbol.markets", 58 | "ts":1535445731347 59 | } 60 | ``` 61 | 62 | > 便于阅读,以下socket订阅api均先返回订阅成功信息后,再推送订阅数据。 63 | 64 | ### 取消订阅 65 | 66 | ***客户端*** 67 | 取消当前订阅 68 | ```js 69 | {"unsub":"market.$symbol.markets"} 70 | ``` 71 | 72 | ***服务端*** 73 | 返回取消订阅成功消息 74 | 75 | ```js 76 | { 77 | "id":"", 78 | "status":"ok", 79 | "unsubbed":"market.usdt.markets", 80 | "ts":1535447792757 81 | } 82 | ``` 83 | ## API 84 | 85 | ### 市场行情 86 | 87 | 订阅锚定货币的所有行情 88 | 89 | - $symbol 交易对。可选:usdt/btc/eth 90 | 91 | ```js 92 | {"sub":"market.$symbol.markets"} 93 | ``` 94 | 95 | *订阅成功返回* 96 | 97 | ```js 98 | { 99 | "id":"", 100 | "status":"ok", 101 | "subbed":"market.$symbol.markets", 102 | "ts":1535445731347 103 | } 104 | ``` 105 | 106 | *推送返回* 107 | - coins: 币种、最新价、涨幅 108 | 109 | ```js 110 | { 111 | "ch":"market.usdt.markets", 112 | "ts":1535445731347, 113 | "tick":{ 114 | "coins":[ 115 | ["ENB",1,1], 116 | ["ECHO",0,0] 117 | ] 118 | } 119 | } 120 | ``` 121 | 122 | ### 交易对最新行情 123 | 124 | 提供交易对名称,订阅该最新交易数据 125 | 126 | - $symbol 交易对。如:ethusdt/btcusdt/etceth 127 | 128 | ```js 129 | {"sub":"market.$symbol.latest"} 130 | ``` 131 | 132 | *返回* 133 | 134 | - 当前价、涨跌幅、最高价、最低价、24小时成交量 135 | 136 | ```js 137 | { 138 | "ch":"market.eth.latest", 139 | "ts":1535445731844, 140 | "tick":{ 141 | "latest":[281.51,-1.67,292.08,269.48,98794] 142 | } 143 | } 144 | ``` 145 | 146 | ### 盘口数据 147 | 148 | 订阅买卖上方挂盘数据 149 | 150 | - $symbol 交易对 151 | 152 | ```js 153 | {"sub":"market.$symbol.orders"} 154 | ``` 155 | 156 | *返回* 157 | - sell: 卖盘 [价格、量、累计] *价格降序* 158 | - buy: 买盘 [价格、量、累计] *价格升序* 159 | 160 | ```js 161 | { 162 | "ch":"market.eth.orders", 163 | "ts":1535445731844, 164 | "tick":{ 165 | "sell":[ 166 | [281.96,0.5,5.5038], 167 | [282,0.5,5.5038], 168 | ... 169 | ], 170 | "buy":[ 171 | [282.37,3.0309,7.8876], 172 | ... 173 | ] 174 | } 175 | } 176 | ``` 177 | 178 | ### 成交明细 179 | 180 | 订阅实时成交 181 | 182 | ```js 183 | {"sub":"market.$symbol.deals"} 184 | ``` 185 | 186 | *返回* 187 | 188 | [timestamp, 方向, 成交价, 数量] 189 | 190 | ```js 191 | { 192 | "ch":"market.ethusdt.deals", 193 | "ts":1535529998712, 194 | "tick":{ 195 | "deals":[ 196 | [1535529998712,0,100,200], 197 | ... 198 | ] 199 | } 200 | } 201 | ``` 202 | 203 | ### 挂盘深度 204 | 205 | 订阅市场深度。为了让图表显示可读性强,**买卖挂盘价格边界(买盘最低价和卖盘最高价)必须保持对称,并且有挂盘量。** 206 | 207 | ```js 208 | {"sub":"market.$symbol.depth"} 209 | ``` 210 | 211 | *返回* 212 | 213 | ```js 214 | { 215 | "ch":"market.ethusdt.depth", 216 | "ts":1535529998712, 217 | "tick":{ 218 | bids: [ 219 | [999, 20], 220 | ... 221 | ], // 买1价,买1量 222 | asks: [ 223 | [0, 0], 224 | ... 225 | ], // 卖1价,卖1量 226 | } 227 | } 228 | ``` 229 | -------------------------------------------------------------------------------- /antd-theme/theme.less: -------------------------------------------------------------------------------- 1 | // 这个主题会批量替换./node_modules/antd/lib/style/themes/default.less中的属性 2 | @ant-prefix : ant; 3 | 4 | @primary-1: color(~`colorPalette('@{primary-color}', 10)`); 5 | @font-size-base: 12px; 6 | @text-color: #c7cce6; 7 | @heading-color: #c7cce6; 8 | @text-color-secondary: #4e5b85; // table 排序图标 9 | @background-color-base: transparent; // 排序选中的背景 10 | @table-header-bg: none; 11 | @table-padding-vertical: 3px; 12 | @table-padding-horizontal: 8px; 13 | @border-color-base: #4e5b85; 14 | @input-bg: #1e2235; 15 | @component-background: transparent; // no data - placeholder 16 | @border-color-split: #1f2943; 17 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebookincubator/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 14 | 15 | 16 | 25 | React App 26 | 27 | 28 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = 30 | FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // First, read the current file sizes in build directory. 44 | // This lets us display how much they changed later. 45 | measureFileSizesBeforeBuild(paths.appBuild) 46 | .then(previousFileSizes => { 47 | // Remove all content but keep the directory so that 48 | // if you're in it, you don't end up in Trash 49 | fs.emptyDirSync(paths.appBuild); 50 | // Merge with the public folder 51 | copyPublicFolder(); 52 | // Start the webpack build 53 | return build(previousFileSizes); 54 | }) 55 | .then( 56 | ({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | console.log(chalk.yellow('Compiled with warnings.\n')); 59 | console.log(warnings.join('\n\n')); 60 | console.log( 61 | '\nSearch for the ' + 62 | chalk.underline(chalk.yellow('keywords')) + 63 | ' to learn more about each warning.' 64 | ); 65 | console.log( 66 | 'To ignore, add ' + 67 | chalk.cyan('// eslint-disable-next-line') + 68 | ' to the line before.\n' 69 | ); 70 | } else { 71 | console.log(chalk.green('Compiled successfully.\n')); 72 | } 73 | 74 | console.log('File sizes after gzip:\n'); 75 | printFileSizesAfterBuild( 76 | stats, 77 | previousFileSizes, 78 | paths.appBuild, 79 | WARN_AFTER_BUNDLE_GZIP_SIZE, 80 | WARN_AFTER_CHUNK_GZIP_SIZE 81 | ); 82 | console.log(); 83 | 84 | const appPackage = require(paths.appPackageJson); 85 | const publicUrl = paths.publicUrl; 86 | const publicPath = config.output.publicPath; 87 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 88 | printHostingInstructions( 89 | appPackage, 90 | publicUrl, 91 | publicPath, 92 | buildFolder, 93 | useYarn 94 | ); 95 | }, 96 | err => { 97 | console.log(chalk.red('Failed to compile.\n')); 98 | printBuildError(err); 99 | process.exit(1); 100 | } 101 | ); 102 | 103 | // Create the production build and print the deployment instructions. 104 | function build(previousFileSizes) { 105 | console.log('Creating an optimized production build...'); 106 | 107 | let compiler = webpack(config); 108 | return new Promise((resolve, reject) => { 109 | compiler.run((err, stats) => { 110 | if (err) { 111 | return reject(err); 112 | } 113 | const messages = formatWebpackMessages(stats.toJson({}, true)); 114 | if (messages.errors.length) { 115 | // Only keep the first error. Others are often indicative 116 | // of the same problem, but confuse the reader with noise. 117 | if (messages.errors.length > 1) { 118 | messages.errors.length = 1; 119 | } 120 | return reject(new Error(messages.errors.join('\n\n'))); 121 | } 122 | if ( 123 | process.env.CI && 124 | (typeof process.env.CI !== 'string' || 125 | process.env.CI.toLowerCase() !== 'false') && 126 | messages.warnings.length 127 | ) { 128 | console.log( 129 | chalk.yellow( 130 | '\nTreating warnings as errors because process.env.CI = true.\n' + 131 | 'Most CI servers set it automatically.\n' 132 | ) 133 | ); 134 | return reject(new Error(messages.warnings.join('\n\n'))); 135 | } 136 | return resolve({ 137 | stats, 138 | previousFileSizes, 139 | warnings: messages.warnings, 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | function copyPublicFolder() { 146 | fs.copySync(paths.appPublic, paths.appBuild, { 147 | dereference: true, 148 | filter: file => file !== paths.appHtml, 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'development'; 3 | process.env.NODE_ENV = 'development'; 4 | 5 | // Makes the script crash on unhandled rejections instead of silently 6 | // ignoring them. In the future, promise rejections that are not handled will 7 | // terminate the Node.js process with a non-zero exit code. 8 | process.on('unhandledRejection', err => { 9 | throw err; 10 | }); 11 | 12 | // Ensure environment variables are read. 13 | require('../config/env'); 14 | 15 | const fs = require('fs'); 16 | const chalk = require('chalk'); 17 | const webpack = require('webpack'); 18 | const WebpackDevServer = require('webpack-dev-server'); 19 | const clearConsole = require('react-dev-utils/clearConsole'); 20 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 21 | const { 22 | choosePort, 23 | createCompiler, 24 | prepareProxy, 25 | prepareUrls, 26 | } = require('react-dev-utils/WebpackDevServerUtils'); 27 | const openBrowser = require('react-dev-utils/openBrowser'); 28 | const paths = require('../config/paths'); 29 | const config = require('../config/webpack.config.dev'); 30 | const createDevServerConfig = require('../config/webpackDevServer.config'); 31 | 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | const isInteractive = process.stdout.isTTY; 34 | 35 | // Warn and crash if required files are missing 36 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 37 | process.exit(1); 38 | } 39 | 40 | // Tools like Cloud9 rely on this. 41 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 42 | const HOST = process.env.HOST || '0.0.0.0'; 43 | 44 | if (process.env.HOST) { 45 | console.log( 46 | chalk.cyan( 47 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 48 | chalk.bold(process.env.HOST), 49 | )}`, 50 | ), 51 | ); 52 | console.log( 53 | `If this was unintentional, check that you haven't mistakenly set it in your shell.`, 54 | ); 55 | console.log(`Learn more here: ${chalk.yellow('http://bit.ly/2mwWSwH')}`); 56 | console.log(); 57 | } 58 | 59 | // We attempt to use the default port but if it is busy, we offer the user to 60 | // run on a different port. `choosePort()` Promise resolves to the next free port. 61 | choosePort(HOST, DEFAULT_PORT) 62 | .then(port => { 63 | if (port == null) { 64 | // We have not found a port. 65 | return; 66 | } 67 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 68 | const appName = require(paths.appPackageJson).name; 69 | const urls = prepareUrls(protocol, HOST, port); 70 | // Create a webpack compiler that is configured with custom messages. 71 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 72 | // Load proxy config 73 | const proxySetting = require(paths.appPackageJson).proxy; 74 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 75 | // Serve webpack assets generated by the compiler over a web sever. 76 | const serverConfig = createDevServerConfig( 77 | proxyConfig, 78 | urls.lanUrlForConfig, 79 | ); 80 | const devServer = new WebpackDevServer(compiler, serverConfig); 81 | devServer.listen(port, HOST, err => { 82 | if (err) { 83 | return console.log(err); 84 | } 85 | if (isInteractive) { 86 | clearConsole(); 87 | } 88 | console.log(chalk.cyan('Starting the development server...\n')); 89 | openBrowser(urls.localUrlForBrowser); 90 | }); 91 | 92 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 93 | process.on(sig, function() { 94 | devServer.close(); 95 | process.exit(); 96 | }); 97 | }); 98 | }) 99 | .catch(err => { 100 | if (err && err.message) { 101 | console.log(err.message); 102 | } 103 | process.exit(1); 104 | }); 105 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | let argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | const constant = { 2 | port: 8000, // 监听端口 3 | allowDomain: 'http://localhost:3000', 4 | defaultRowNum: 3000, // K线初始记录数 5 | intervalTime: 1000, // 定时推送时间间隔 6 | }; 7 | 8 | module.exports = { 9 | constant, 10 | }; 11 | -------------------------------------------------------------------------------- /server/controller/kline.js: -------------------------------------------------------------------------------- 1 | const mock = require('../mock'); 2 | 3 | exports.kline = ctx => { 4 | const json = ctx.request.body; 5 | const [, symbol, channel, period] = json.req.split('.'); 6 | const data = mock.kline.getAll(period); 7 | ctx.body = { 8 | rep: `market.${symbol}.${channel}.${period}`, 9 | status: 'ok', 10 | id: json.id || '', 11 | tick: data, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /server/controller/user.js: -------------------------------------------------------------------------------- 1 | exports.userData = ctx => { 2 | ctx.body = { 3 | status: 'ok', 4 | data: { 5 | assets: { 6 | usdt: 1, 7 | btc: 0, 8 | eth: 1, 9 | etc: 2, 10 | }, 11 | }, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const bodyParser = require('koa-bodyparser'); 3 | const onError = require('koa-onerror'); 4 | const logger = require('koa-logger'); 5 | const WebSocket = require('ws'); 6 | 7 | const config = require('./config'); 8 | const socket = require('./socket'); 9 | const router = require('./router'); 10 | 11 | const app = new Koa(); 12 | const server = require('http').createServer(app.callback()); 13 | const wss = new WebSocket.Server({ server, path: '/ws' }); 14 | 15 | socket(wss); 16 | 17 | onError(app); 18 | app.use( 19 | bodyParser({ 20 | enableTypes: ['json', 'form', 'text'], 21 | }), 22 | ); 23 | 24 | app.use(async (ctx, next) => { 25 | const start = Date.now(); 26 | await next(); 27 | const ms = Date.now() - start; 28 | ctx.set('X-Response-Time', `${ms}ms`); 29 | }); 30 | 31 | app.use(async (ctx, next) => { 32 | await next(); 33 | ctx.set({ 34 | 'Access-Control-Allow-Origin': config.constant.allowDomain, 35 | 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', 36 | 'Access-Control-Allow-Headers': 37 | 'x-requested-with, accept, origin, content-type', 38 | 'Access-Control-Allow-Credentials': 'true', 39 | }); 40 | 41 | if (ctx.request.method === 'OPTIONS') { 42 | ctx.response.status = 200; 43 | } 44 | }); 45 | 46 | app.use(logger()); 47 | 48 | app.use(router.routes()); 49 | 50 | server.listen(config.constant.port, () => { 51 | console.log('listen on ' + config.constant.port); 52 | }); 53 | -------------------------------------------------------------------------------- /server/mock/index.js: -------------------------------------------------------------------------------- 1 | const kline = require('./kline'); 2 | 3 | module.exports = { 4 | kline, 5 | }; 6 | -------------------------------------------------------------------------------- /server/mock/kline.js: -------------------------------------------------------------------------------- 1 | const utils = require('./lib/utils'); 2 | const config = require('../config'); 3 | 4 | const now = new Date().getTime(); 5 | 6 | const getAll = period => { 7 | const step = utils.getMillisecond(period) / 3; 8 | const params = { 9 | step, 10 | length: config.constant.defaultRowNum, 11 | type: 'prev', 12 | startTime: from, 13 | endTime: to, 14 | }; 15 | 16 | let result = utils.getMockData(params); 17 | let returnData = {}; 18 | let compareTime = now - step; 19 | 20 | if (compareTime >= from) { 21 | returnData = { 22 | code: 0, 23 | type: 'kline', 24 | data: { 25 | kLine: { 26 | ...result, 27 | s: 'ok', 28 | }, 29 | }, 30 | }; 31 | } else { 32 | returnData = { 33 | code: 0, 34 | type: 'kline', 35 | data: { 36 | kLine: { 37 | t: [], 38 | c: [], 39 | o: [], 40 | h: [], 41 | l: [], 42 | v: [], 43 | s: 'ok', 44 | }, 45 | }, 46 | }; 47 | } 48 | 49 | // 发送历史数据 50 | ws.send(JSON.stringify(returnData)); 51 | }; 52 | 53 | // 追加一条kline数据 54 | function append({ step, endTime, delta }) { 55 | const params = { 56 | step, 57 | length: 1, 58 | type: 'next', 59 | delta, 60 | endTime, 61 | }; 62 | 63 | const result = utils.getMockData(params); 64 | let { t, c, o, h, l, v } = result; 65 | return { 66 | code: 0, 67 | type: 'dealSuccess', 68 | data: { 69 | kLine: { 70 | t: t[0], 71 | c: c[0], 72 | o: o[0], 73 | h: h[0], 74 | l: l[0], 75 | v: v[0], 76 | s: 'ok', 77 | }, 78 | }, 79 | }; 80 | } 81 | 82 | module.exports = { 83 | getAll, 84 | append, 85 | }; 86 | -------------------------------------------------------------------------------- /server/mock/lib/utils.js: -------------------------------------------------------------------------------- 1 | const dayTime = 60 * 60 * 24 * 1000; 2 | const periodMatch = /^(\d{1,2})(min|hour|day|week|mon|year)$/; 3 | const vnum = 2554477; 4 | 5 | const getMillisecond = period => { 6 | let number = 0; 7 | let millisecond = 0; 8 | 9 | const matches = period.match(periodMatch); 10 | if (matches) { 11 | number = matches[1]; 12 | type = matches[2]; 13 | } 14 | 15 | switch (type) { 16 | case 'min': 17 | millisecond = number * 60 * 1000; 18 | break; 19 | case 'hour': 20 | millisecond = number * 60 * 60 * 1000; 21 | break; 22 | case 'day': 23 | millisecond = number * dayTime; 24 | break; 25 | case 'week': 26 | millisecond = number * dayTime * 7; 27 | break; 28 | case 'mon': 29 | millisecond = number * dayTime * 30; 30 | break; 31 | case 'year': 32 | millisecond = number * dayTime * 365; 33 | break; 34 | default: 35 | millisecond = 0; 36 | } 37 | 38 | return millisecond; 39 | }; 40 | 41 | function getRandomNum(integer, decimal = 0) { 42 | const randomNum = 43 | Math.pow(10, integer) + Math.random() * Math.pow(10, integer); 44 | return randomNum.toFixed(decimal) * 1; 45 | } 46 | 47 | function getMockData(params) { 48 | const { step, length, type, delta, endTime } = params; 49 | const t = []; 50 | const c = []; 51 | const o = []; 52 | const h = []; 53 | const l = []; 54 | const v = []; 55 | 56 | for (let i = 0; i < length; i++) { 57 | const num = getRandomNum(2, 2); 58 | h.push(num); 59 | l.push(num - getRandomNum(1)); 60 | o.push(num - getRandomNum(1)); 61 | c.push(num - getRandomNum(1)); 62 | v.push(vnum - getRandomNum(6)); 63 | 64 | if (type === 'prev') { 65 | const time = Math.floor((endTime - step * (i + 1)) / 1000); 66 | t.unshift(time); 67 | } else if (type === 'next') { 68 | const time = Math.floor( 69 | (endTime + step * (delta + i + 1)) / 1000, 70 | ); 71 | t.push(time); 72 | } 73 | } 74 | 75 | return { 76 | t, 77 | c, 78 | o, 79 | h, 80 | l, 81 | v, 82 | }; 83 | } 84 | 85 | module.exports = { 86 | getMillisecond, 87 | getMockData, 88 | }; 89 | -------------------------------------------------------------------------------- /server/router/exchange.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | const api = require('../controller/kline'); 3 | 4 | const router = new Router(); 5 | router.get('/kline', api.kline); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /server/router/index.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | 3 | const user = require('./user'); 4 | const exchange = require('./exchange'); 5 | 6 | const router = new Router(); 7 | 8 | router.use('/api/user', user.routes()); 9 | router.use('/api/exchange', exchange.routes()); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /server/router/user.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | const api = require('../controller/user'); 3 | 4 | const router = new Router(); 5 | router.get('/user-data', api.userData); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /server/socket.js: -------------------------------------------------------------------------------- 1 | const mock = require('./mock'); 2 | 3 | function send(socket, data) { 4 | console.log('send', JSON.stringify(data)); 5 | socket.send(JSON.stringify(data)); 6 | } 7 | 8 | function subHandle(socket, symbol, extraArgs, data, channel) { 9 | let rest = ''; 10 | if (extraArgs.length > 0) { 11 | rest = '.' + extraArgs.join('.'); 12 | } 13 | 14 | send(socket, { 15 | id: data.id || '', 16 | status: 'ok', 17 | subbed: `market.${symbol}.${channel}${rest}`, 18 | ts: Date.now(), 19 | }); 20 | } 21 | 22 | function unsubHandle(socket, symbol, extraArgs, data, channel) { 23 | let rest = ''; 24 | if (extraArgs.length > 0) { 25 | rest = '.' + extraArgs.join('.'); 26 | } 27 | 28 | send(socket, { 29 | id: data.id || '', 30 | status: 'ok', 31 | unsubbed: `market.${symbol}.${channel}${rest}`, 32 | ts: Date.now(), 33 | }); 34 | } 35 | 36 | function marketsHandle(socket, symbol, extraArgs, data) { 37 | subHandle(socket, symbol, extraArgs, data, 'markets'); 38 | 39 | const markets = { 40 | usdt: [['ENB', 1, 1], ['ECHO', 0, -1]], 41 | btc: [['ETH', 2, 1.05]], 42 | eth: [['ENB', 1, 1]], 43 | }; 44 | 45 | send(socket, { 46 | ch: `market.${symbol}.markets`, 47 | ts: Date.now(), 48 | tick: { 49 | markets: markets[symbol], 50 | }, 51 | }); 52 | } 53 | 54 | function latestHandle(socket, symbol, extraArgs, data) { 55 | subHandle(socket, symbol, extraArgs, data, 'latest'); 56 | 57 | send(socket, { 58 | ch: `market.${symbol}.latest`, 59 | ts: Date.now(), 60 | tick: { 61 | latest: [0.007, -1.67, 292.08, 269.48, 98794], 62 | }, 63 | }); 64 | } 65 | 66 | function ordersHandle(socket, symbol, extraArgs, data) { 67 | subHandle(socket, symbol, extraArgs, data, 'orders'); 68 | 69 | send(socket, { 70 | ch: `market.${symbol}.orders`, 71 | ts: Date.now(), 72 | tick: { 73 | sell: [[281.96, 0.5, 5.5038]], 74 | buy: [[282.37, 3.0309, 7.8876]], 75 | }, 76 | }); 77 | } 78 | 79 | function dealsHandle(socket, symbol, extraArgs, data) { 80 | subHandle(socket, symbol, extraArgs, data, 'deals'); 81 | 82 | send(socket, { 83 | ch: `market.${symbol}.deals`, 84 | ts: Date.now(), 85 | tick: { 86 | deals: [[Date.now(), 0, 100, 200], [Date.now(), 1, 50, 221]], 87 | }, 88 | }); 89 | } 90 | 91 | function depthHandle(socket, symbol, extraArgs, data) { 92 | subHandle(socket, symbol, extraArgs, data, 'depth'); 93 | 94 | send(socket, { 95 | ch: `market.${symbol}.depth`, 96 | ts: Date.now(), 97 | tick: { 98 | bids: [[5, 10], [6, 9], [7, 8], [8, 7], [9, 1]], 99 | asks: [[9, 1], [10, 2], [11, 3], [12, 40], [13, 41.5]], 100 | }, 101 | }); 102 | } 103 | 104 | function klineHandle(socket, symbol, extraArgs, data) { 105 | subHandle(socket, symbol, extraArgs, data, 'kline'); 106 | 107 | const [period] = extraArgs; 108 | //const tick = mock.kline.getAll(period); 109 | send(socket, { 110 | ch: `market.${symbol}.kline.${period}`, 111 | ts: Date.now(), 112 | tick: {}, 113 | }); 114 | } 115 | 116 | module.exports = ws => { 117 | ws.on('connection', socket => { 118 | console.log('connected!'); 119 | socket.on('message', text => { 120 | const data = JSON.parse(text); 121 | console.log('receive', text); 122 | if (data.hasOwnProperty('sub')) { 123 | // 订阅 124 | const [, symbol, channel, ...extraArgs] = data.sub.split('.'); 125 | switch (channel) { 126 | case 'markets': 127 | marketsHandle(socket, symbol, extraArgs, data); 128 | break; 129 | case 'latest': 130 | latestHandle(socket, symbol, extraArgs, data); 131 | break; 132 | case 'orders': 133 | ordersHandle(socket, symbol, extraArgs, data); 134 | break; 135 | case 'deals': 136 | dealsHandle(socket, symbol, extraArgs, data); 137 | break; 138 | case 'depth': 139 | depthHandle(socket, symbol, extraArgs, data); 140 | break; 141 | case 'kline': 142 | klineHandle(socket, symbol, extraArgs, data); 143 | break; 144 | } 145 | } else if (data.hasOwnProperty('unsub')) { 146 | const [, symbol, channel, ...extraArgs] = data.unsub.split('.'); 147 | unsubHandle(socket, symbol, extraArgs, data, channel); 148 | } 149 | }); 150 | }); 151 | }; 152 | -------------------------------------------------------------------------------- /src/actions/exchangeActions.js: -------------------------------------------------------------------------------- 1 | export function setSymbol(symbol) { 2 | return { 3 | type: 'EXCHANGE.SET_SYMBOL', 4 | payload: symbol, 5 | }; 6 | } 7 | 8 | export function changeSearchWord(value) { 9 | return { 10 | type: 'EXCHANGE.CHANGE_SEARCH_WORD', 11 | payload: value, 12 | }; 13 | } 14 | 15 | export function subscribeMarkets(symbol) { 16 | return { 17 | type: 'EXCHANGE.SUBSCRIBE_MARKETS', 18 | payload: symbol, 19 | }; 20 | } 21 | 22 | export function switchMarkets(symbol) { 23 | return { 24 | type: 'EXCHANGE.SWITCH_MARKETS', 25 | payload: symbol, 26 | }; 27 | } 28 | 29 | export function marketsComplete(data) { 30 | return { 31 | type: 'EXCHANGE.MARKETS_COMPLETE', 32 | payload: data, 33 | }; 34 | } 35 | 36 | export function subscribeLatest(symbol) { 37 | return { 38 | type: 'EXCHANGE.SUBSCRIBE_LATEST', 39 | payload: symbol, 40 | }; 41 | } 42 | 43 | export function latestComplete(data) { 44 | return { 45 | type: 'EXCHANGE.LATEST_COMPLETE', 46 | payload: data, 47 | }; 48 | } 49 | 50 | // 对手盘 51 | export function subscribeOrders(symbol) { 52 | return { 53 | type: 'EXCHANGE.SUBSCRIBE_ORDERS', 54 | payload: symbol, 55 | }; 56 | } 57 | 58 | export function ordersComplete(data) { 59 | return { 60 | type: 'EXCHANGE.ORDERS_COMPLETE', 61 | payload: data, 62 | }; 63 | } 64 | 65 | export function subscribeDeals(symbol) { 66 | return { 67 | type: 'EXCHANGE.SUBSCRIBE_DEALS', 68 | payload: symbol, 69 | }; 70 | } 71 | 72 | export function dealsComplete(data) { 73 | return { 74 | type: 'EXCHANGE.DEALS_COMPLETE', 75 | payload: data, 76 | }; 77 | } 78 | 79 | export function subscribeDepth(symbol) { 80 | return { 81 | type: 'EXCHANGE.SUBSCRIBE_DEPTH', 82 | payload: symbol, 83 | }; 84 | } 85 | 86 | export function depthComplete(data) { 87 | return { 88 | type: 'EXCHANGE.DEPTH_COMPLETE', 89 | payload: data, 90 | }; 91 | } 92 | 93 | export function subscribeKLine(symbol) { 94 | return { 95 | type: 'EXCHANGE.SUBSCRIBE_KLINE', 96 | payload: symbol, 97 | }; 98 | } 99 | 100 | export function klineComplete(data) { 101 | return { 102 | type: 'EXCHANGE.KLINE_COMPLETE', 103 | payload: data, 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/actions/globalActions.js: -------------------------------------------------------------------------------- 1 | export function clearSession() { 2 | return { 3 | type: 'GLOBAL.CLEAR_SESSION', 4 | }; 5 | } 6 | 7 | export function changeLanguage(ethereum, lang, locale) { 8 | return { 9 | type: 'GLOBAL.CHANGE_LANGUAGE', 10 | payload: { ethereum, lang, locale }, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export const INIT_DEPTH_DATA = 'INIT_DEPTH_DATA'; 2 | export const DEPTH_DATA_ADD = 'DEPTH_DATA_ADD'; 3 | -------------------------------------------------------------------------------- /src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | export function getUserdata() { 2 | return { 3 | type: 'USER.GET_USER_DATA', 4 | payload: '/user-data', 5 | }; 6 | } 7 | 8 | export function userDataComplete(data) { 9 | return { 10 | type: 'USER.USER_DATA_COMPLETE', 11 | payload: data.assets, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Layout/LayoutView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | import { ConnectedRouter } from 'connected-react-router'; 4 | 5 | import constants from '../../services/constants'; 6 | 7 | import './LayoutView.css'; 8 | 9 | const LayoutView = props => { 10 | let defaultPathExchange = constants.BASE_HOST + '/exchange/eth_usdt'; 11 | if (props.currentLanguage !== 'en') { 12 | defaultPathExchange += '?lang=' + props.currentLanguage; 13 | } 14 | 15 | return ( 16 | 17 |
18 | 19 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default LayoutView; 32 | -------------------------------------------------------------------------------- /src/components/Layout/LayoutView.scss: -------------------------------------------------------------------------------- 1 | // 全局样式 2 | ul, 3 | li { 4 | padding: 0; 5 | margin: 0; 6 | } 7 | 8 | li { 9 | list-style: none; 10 | } 11 | 12 | :global { 13 | .color-up { 14 | color: #589065; 15 | 16 | &::before { 17 | content: '+'; 18 | } 19 | } 20 | 21 | .color-down { 22 | color: #ae4e54; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getTranslate, getActiveLanguage } from 'react-localize-redux'; 4 | import throttle from 'lodash/throttle'; 5 | 6 | import Exchange from '../../pages/Exchange/'; 7 | import history from '../../history'; 8 | import { clearession } from '../../actions/globalActions'; 9 | import LayoutView from './LayoutView'; 10 | 11 | @connect(store => { 12 | return { 13 | translate: getTranslate(store.locale), 14 | currentLanguage: getActiveLanguage(store.locale).code, 15 | }; 16 | }) 17 | export default class extends React.Component { 18 | constructor() { 19 | super(); 20 | this.idleTime = 0; 21 | this.timeoutEndSession = 90; 22 | this.intervalIdle = null; 23 | } 24 | 25 | componentDidMount() { 26 | document.addEventListener('mousemove', this.resetTimer, { passive: true }); 27 | document.addEventListener('touchstart', this.resetTimer, { passive: true }); 28 | document.addEventListener('keypress', this.resetTimer, { passive: true }); 29 | this.intervalIdle = setInterval(this.checkTimer, 10000); 30 | } 31 | 32 | componentWillUnmount() { 33 | document.removeEventListener('mousemove', this.resetTimer); 34 | document.removeEventListener('touchstart', this.resetTimer); 35 | document.removeEventListener('keypress', this.resetTimer); 36 | clearInterval(this.intervalIdle); 37 | } 38 | 39 | checkTimer = () => { 40 | if (this.idleTime >= this.timeoutEndSession) { 41 | this.endSession(); 42 | } else { 43 | this.idleTime++; 44 | } 45 | }; 46 | 47 | resetTimer = throttle(() => { 48 | this.idleTime = 0; 49 | }, 5000); 50 | 51 | endSession() { 52 | this.props.dispatch(clearSession()); 53 | } 54 | 55 | render() { 56 | return ( 57 | 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/history.js: -------------------------------------------------------------------------------- 1 | import createHistory from 'history/createBrowserHistory'; 2 | // import * as analytics from './utils/analytics' 3 | 4 | //const history = createHistory() 5 | const history = createHistory({ 6 | basename: '', 7 | hashType: 'slash', 8 | }); 9 | 10 | /*history.listen(function (location) { 11 | analytics.changePath(location.pathname + location.search) 12 | })*/ 13 | export default history; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { PersistGate } from 'redux-persist/lib/integration/react'; 5 | 6 | import registerServiceWorker from './registerServiceWorker'; 7 | import { store, persistor } from './store'; 8 | import Layout from './components/Layout'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root'), 17 | ); 18 | 19 | registerServiceWorker(); 20 | -------------------------------------------------------------------------------- /src/pages/Exchange/Deals/DealsView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col, Icon } from 'antd'; 3 | import dayjs from 'dayjs'; 4 | import uuid from 'uuid/v1'; 5 | import cs from 'classnames'; 6 | 7 | import './DealsView.css'; 8 | 9 | function toTime(timestamp) { 10 | return dayjs(timestamp).format('HH:mm:ss'); 11 | } 12 | 13 | const types = ['买', '卖']; 14 | function toType(bit) { 15 | return types[bit]; 16 | } 17 | 18 | const Deals = ({ lists }) => { 19 | return lists.map(list => { 20 | return ( 21 |
22 | 23 | {toTime(list[0])} 24 | 31 | {toType(list[1])} 32 | 33 | {list[2].toFixed(2)} 34 | {list[3].toFixed(4)} 35 | 36 |
37 | ); 38 | }); 39 | }; 40 | 41 | const DealsView = ({ translate, deals, coin }) => { 42 | return ( 43 |
44 |
45 | 46 | 实时成交 47 |
48 |
49 |
50 | 51 | 时间 52 | 方向 53 | 价格(USDT) 54 | 55 | 数量( 56 | {coin}) 57 | 58 | 59 |
60 | 61 |
62 |
63 | ); 64 | }; 65 | export default DealsView; 66 | -------------------------------------------------------------------------------- /src/pages/Exchange/Deals/DealsView.scss: -------------------------------------------------------------------------------- 1 | @import '../common.scss'; 2 | 3 | .container { 4 | font-size: 12px; 5 | 6 | dl { 7 | margin: .7em 20px; 8 | } 9 | 10 | dt div { 11 | margin-bottom: 5px; 12 | color: #61688a; 13 | } 14 | 15 | .text-right { 16 | text-align: right; 17 | 18 | div:first-child, 19 | div:nth-child(2) { 20 | text-align: left; 21 | } 22 | } 23 | 24 | .color-sell { 25 | color: #ae4e54; 26 | } 27 | 28 | .color-buy { 29 | color: #589065; 30 | } 31 | 32 | .header { 33 | @extend %win-header; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/Exchange/Deals/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getTranslate } from 'react-localize-redux/lib/index'; 4 | 5 | import DealsView from './DealsView'; 6 | import { subscribeDeals } from '../../../actions/exchangeActions'; 7 | 8 | @connect(store => { 9 | return { 10 | translate: getTranslate(store.locale), 11 | deals: store.exchange.deals, 12 | symbol: store.exchange.configs.symbol, 13 | }; 14 | }) 15 | export default class extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | 19 | this.symbol = ''; 20 | } 21 | 22 | componentWillReceiveProps(nextProps) { 23 | if (this.symbol !== nextProps.symbol) { 24 | this.symbol = nextProps.symbol; 25 | this.props.dispatch(subscribeDeals(this.symbol)); 26 | } 27 | } 28 | 29 | render() { 30 | return ( 31 | 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/Exchange/Depth/DepthView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactEchartsCore from 'echarts-for-react/lib/core'; 3 | import echarts from 'echarts/lib/echarts'; 4 | import 'echarts/lib/chart/line'; 5 | import 'echarts/lib/component/tooltip'; 6 | import { Icon } from 'antd'; 7 | 8 | import './DepthView.css'; 9 | 10 | export default class extends React.Component { 11 | echartsReact = React.createRef(); 12 | echartsInstance = null; 13 | 14 | componentWillReceiveProps(nextProps) { 15 | this.echartsInstance.setOption(nextProps.depth); 16 | } 17 | 18 | componentDidMount() { 19 | this.echartsInstance = this.echartsReact.current.getEchartsInstance(); 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |
26 | 27 | 深度图 28 |
29 | 35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/Exchange/Depth/DepthView.scss: -------------------------------------------------------------------------------- 1 | @import '../common.scss'; 2 | 3 | .container { 4 | height: calc(100% - 50px); 5 | font-size: 12px; 6 | 7 | .header { 8 | @extend %win-header; 9 | } 10 | 11 | :global { 12 | .tooltip { 13 | min-width: 100px; 14 | border-radius: 4px; 15 | color: #fff; 16 | font-size: 12px; 17 | 18 | span { 19 | float: left; 20 | width: 60px; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/Exchange/Depth/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getTranslate } from 'react-localize-redux/lib/index'; 4 | 5 | import DepthView from './DepthView'; 6 | import { subscribeDepth } from '../../../actions/exchangeActions'; 7 | 8 | const option = { 9 | tooltip: { 10 | confine: true, 11 | trigger: 'axis', 12 | axisPointer: { type: 'line', lineStyle: { color: 'rgba(0, 0, 0, 0)' } }, 13 | backgroundColor: 'rgb(38, 42, 66)', 14 | padding: 10, 15 | extraCssText: 16 | 'box-shadow: 0 0 5px 0 rgba(0, 0, 0, .3); border-radius: 4px;', 17 | transitionDuration: 0, 18 | formatter([{ value }]) { 19 | return ( 20 | '
' + 21 | `委托价 ${value[0]}` + 22 | '
' + 23 | `累计 ${value[1]}` + 24 | '
' 25 | ); 26 | }, 27 | position(pt, params, dom, rect) {}, 28 | }, 29 | grid: { 30 | top: '30px', 31 | bottom: '20px', 32 | left: '30px', 33 | right: '30px', 34 | }, 35 | xAxis: { 36 | type: 'value', 37 | axisLine: { 38 | onZero: false, 39 | }, 40 | axisLabel: { 41 | showMinLabel: false, // 不显示最小刻度 42 | showMaxLabel: false, 43 | }, 44 | min: 'dataMin', // 折线从数据最小值显示 45 | max: 'dataMax', 46 | splitLine: { show: false }, 47 | }, 48 | yAxis: [ 49 | { 50 | type: 'value', 51 | position: 'right', 52 | axisLine: { 53 | onZero: false, 54 | }, 55 | axisLabel: { 56 | showMaxLabel: false, 57 | }, 58 | splitLine: { show: false }, 59 | }, 60 | ], 61 | series: [ 62 | { 63 | type: 'line', 64 | symbol: 'circle', 65 | showSymbol: false, 66 | sampling: 'average', 67 | itemStyle: { 68 | color: 'rgb(12, 152, 247)', 69 | borderWidth: 8, 70 | borderColor: 'rgba(12, 152, 247, 0.3)', 71 | }, 72 | lineStyle: { normal: { color: '#243235' } }, 73 | areaStyle: { color: '#243235' }, 74 | data: [], 75 | }, 76 | { 77 | type: 'line', 78 | symbol: 'circle', 79 | showSymbol: false, 80 | symbolSize: 8, 81 | hoverAnimation: false, 82 | cursor: 'normal', 83 | sampling: 'average', 84 | itemStyle: { 85 | color: 'rgb(12, 152, 247)', 86 | borderWidth: 8, 87 | borderColor: 'rgba(12, 152, 247, 0.3)', 88 | }, 89 | lineStyle: { normal: { color: '#392332' } }, 90 | areaStyle: { color: '#392332' }, 91 | data: [], 92 | }, 93 | ], 94 | }; 95 | 96 | function getOption({ bids, asks }) { 97 | option.series[0].data = bids; 98 | option.series[1].data = asks; 99 | return option; 100 | } 101 | 102 | @connect(store => { 103 | return { 104 | translate: getTranslate(store.locale), 105 | depth: store.exchange.depth, 106 | symbol: store.exchange.configs.symbol, 107 | }; 108 | }) 109 | export default class extends React.Component { 110 | constructor(props) { 111 | super(props); 112 | 113 | this.symbol = ''; 114 | } 115 | 116 | componentWillReceiveProps(nextProps) { 117 | if (this.symbol !== nextProps.symbol) { 118 | this.symbol = nextProps.symbol; 119 | this.props.dispatch(subscribeDepth(this.symbol)); 120 | } 121 | } 122 | 123 | render() { 124 | return ( 125 | 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/pages/Exchange/Footer/Footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | position: relative; 3 | height: 330px; 4 | margin-top: -330px; 5 | z-index: 2; 6 | 7 | .foot-wrap { 8 | display: flex; 9 | width:90%; 10 | width: 1200px; 11 | padding: 60px 20px 0; 12 | margin: auto; 13 | 14 | .copyright { 15 | padding-top: 60px; 16 | color: #61688a; 17 | } 18 | 19 | dl h2 { 20 | height: 42px; 21 | font-weight: 400; 22 | font-size: 12px; 23 | line-height: 12px; 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/Exchange/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../assets/logo.svg'; 3 | import './Footer.css'; 4 | const Footer = () => { 5 | return ( 6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |

全球领先的数字资产交易平台

14 |
15 |
16 |
17 |
18 |
© 2013-2018 Huobi Global
19 |
20 |
21 |
22 |

服务

23 |
24 |
25 | 26 | 火币资讯 27 | 28 |
29 |
30 | 31 | 火币矿池 32 | 33 |
34 |
35 | 36 | 火币生态 37 | 38 |
39 |
40 | 41 | 火币资本 42 | 43 |
44 |
45 | 46 | 机构账户 47 | 48 |
49 |
50 |
51 |
52 | ); 53 | }; 54 | export default Footer; 55 | -------------------------------------------------------------------------------- /src/pages/Exchange/Grid/GridView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Header from '../Header'; 4 | import Markets from '../Markets'; 5 | import Notice from '../Notice'; 6 | import Mscy from '../Mscy'; 7 | import TradePanel from '../TradePanel'; 8 | import Orders from '../Orders'; 9 | import Deals from '../Deals'; 10 | import KLine from '../KLine'; 11 | import Depth from '../Depth'; 12 | import './GridView.css'; 13 | 14 | const GridView = () => { 15 | return ( 16 |
17 | {/*头部*/} 18 |
19 |
20 |
21 | {/*边栏*/} 22 |
23 | {/*行情*/} 24 | 25 | {/*通知*/} 26 | 27 |
28 | {/*走势图*/} 29 |
30 | 31 |
32 | {/*交易面板*/} 33 |
34 | 35 |
36 | {/*对手盘*/} 37 |
38 | 39 |
40 | {/*深度图*/} 41 |
42 | 43 |
44 | {/*实时成交*/} 45 |
46 | 47 |
48 | {/*币种信息*/} 49 |
50 | 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default GridView; 57 | -------------------------------------------------------------------------------- /src/pages/Exchange/Grid/GridView.scss: -------------------------------------------------------------------------------- 1 | // 去除input[type='number']的上下箭头 2 | input::-webkit-outer-spin-button, 3 | input::-webkit-inner-spin-button { 4 | -webkit-appearance: none; 5 | } 6 | 7 | input[type='number'] { 8 | -moz-appearance: textfield; 9 | } 10 | 11 | .container { 12 | display: grid; 13 | grid-template-rows: 60px 660px 490px 530px 480px; 14 | grid-template-columns: minmax(280px, 360px) minmax(600px, 3fr) minmax(60px, .5fr) minmax(280px, 2fr); 15 | grid-template-areas: 16 | 'h h h h' 17 | 'a c c c' 18 | 'a tp tp o' 19 | 'a d mt mt' 20 | 'a td td td'; 21 | grid-gap: 10px; 22 | padding: 5px; 23 | color: #c7cce6; 24 | background-color: #262a42; 25 | } 26 | 27 | .header { 28 | grid-area: h; 29 | } 30 | 31 | .aside { 32 | grid-area: a; 33 | } 34 | 35 | .line { 36 | grid-area: c; 37 | } 38 | 39 | .trade-panel { 40 | grid-area: tp; 41 | 42 | :global { 43 | .ant-tabs-bar { 44 | border-bottom: none; 45 | } 46 | 47 | .ant-tabs-ink-bar { 48 | background-color: #7a98f7; 49 | } 50 | 51 | .ant-tabs-nav .ant-tabs-tab-active { 52 | border-color: #7a98f7; 53 | color: #7a98f7; 54 | } 55 | 56 | .ant-tabs-tab:hover { 57 | color: #fff; 58 | } 59 | 60 | .ant-tabs-tab-active:hover { 61 | color: #7a98f7; 62 | } 63 | 64 | .ant-tabs-nav { 65 | font-size: 16px; 66 | } 67 | 68 | .ant-tabs-nav-scroll { 69 | padding: 0 30px; 70 | background: #1b1e2e; 71 | box-shadow: 0 3px 6px rgba(0,0,0,.1); 72 | } 73 | } 74 | } 75 | 76 | .orders { 77 | grid-area: o; 78 | } 79 | 80 | .depth { 81 | grid-area: d; 82 | } 83 | 84 | .deals { 85 | grid-area: mt; 86 | } 87 | 88 | .token-details { 89 | grid-area: td; 90 | } 91 | 92 | .header, 93 | .line, 94 | .trade-panel, 95 | .orders, 96 | .depth, 97 | .deals, 98 | .token-details { 99 | border-radius: 5px; 100 | background-color: #181b2a; 101 | } 102 | -------------------------------------------------------------------------------- /src/pages/Exchange/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | height: 100%; 4 | } 5 | 6 | .head-logo { 7 | padding-left: 30px; 8 | margin: 0; 9 | 10 | svg { 11 | margin-top: 15px; 12 | } 13 | } 14 | 15 | .head-nav { 16 | display: flex; 17 | flex: 1; 18 | align-items: center; 19 | padding: 0; 20 | margin: 0 30px 0 0; 21 | font-size: 14px; 22 | 23 | > li { 24 | list-style-type: none; 25 | margin-left: 30px; 26 | 27 | &.head-padding { 28 | flex: 1; 29 | } 30 | 31 | &.head-user { 32 | margin: 0 10px; 33 | } 34 | } 35 | 36 | a { 37 | color: #c7cce6; 38 | text-decoration: none; 39 | transition: all .2s ease-in-out; 40 | 41 | &:hover { 42 | color: #7a98f7; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/Exchange/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './Header.css'; 4 | import Logo from '../assets/logo.svg'; 5 | 6 | const Index = () => { 7 | return ( 8 |
9 |

10 | 11 | 12 | 13 |

14 | 26 |
27 | ); 28 | }; 29 | 30 | export default Index; 31 | -------------------------------------------------------------------------------- /src/pages/Exchange/KLine/KLineView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from 'antd'; 3 | 4 | import './KLineView.css'; 5 | 6 | export default class extends React.Component { 7 | static defaultProps = { 8 | symbol: 'AAPL', 9 | interval: 'D', 10 | containerId: 'tv_chart_container', 11 | datafeedUrl: 'https://demo_feed.tradingview.com', 12 | libraryPath: '/charting_library/', 13 | chartsStorageUrl: 'https://saveload.tradingview.com', 14 | chartsStorageApiVersion: '1.1', 15 | clientId: 'tradingview.com', 16 | userId: 'public_user_id', 17 | fullscreen: false, 18 | autosize: true, 19 | studiesOverrides: {}, 20 | }; 21 | 22 | tvWidget = null; 23 | componentDidMount() { 24 | const widgetOptions = { 25 | symbol: this.props.symbol, 26 | // BEWARE: no trailing slash is expected in feed URL 27 | datafeed: new window.Datafeeds.UDFCompatibleDatafeed( 28 | this.props.datafeedUrl, 29 | ), 30 | interval: this.props.interval, 31 | container_id: this.props.containerId, 32 | library_path: this.props.libraryPath, 33 | 34 | locale: this.props.currentLanguage, 35 | disabled_features: [], 36 | enabled_features: ['study_templates'], 37 | charts_storage_url: this.props.chartsStorageUrl, 38 | charts_storage_api_version: this.props.chartsStorageApiVersion, 39 | client_id: this.props.clientId, 40 | user_id: this.props.userId, 41 | fullscreen: this.props.fullscreen, 42 | autosize: this.props.autosize, 43 | studies_overrides: this.props.studiesOverrides, 44 | theme: 'Dark', 45 | }; 46 | 47 | this.tvWidget = new window.TradingView.widget(widgetOptions); 48 | } 49 | 50 | componentWillUnmount() { 51 | if (this.tvWidget !== null) { 52 | this.tvWidget.remove(); 53 | this.tvWidget = null; 54 | } 55 | } 56 | 57 | render() { 58 | const [price, change, high, low, vol] = this.props.latest; 59 | return ( 60 |
61 |
62 | 63 |
64 |
65 | {this.props.symbol} 66 | {price} 67 |
68 |
69 | ≈ {price} cny 70 |
71 |
72 | 涨幅{' '} 73 | 0 ? 'color-up' : 'color-down'}> 74 | {change}% 75 | 76 |
77 |
78 | 高 {high} 79 |
80 |
81 | 低 {low} 82 |
83 |
84 | 24H量 {vol} ETH 85 |
86 |
87 |
88 |
89 |
90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/pages/Exchange/KLine/KLineView.scss: -------------------------------------------------------------------------------- 1 | @import '../common.scss'; 2 | 3 | .container { 4 | height: calc(100% - 50px); 5 | font-size: 12px; 6 | 7 | .header { 8 | @extend %win-header; 9 | 10 | dl { 11 | display: inline-block; 12 | height: 48px; 13 | padding-left: 20px; 14 | font-style: normal; 15 | line-height: 48px; 16 | white-space: nowrap; 17 | text-transform: uppercase; 18 | 19 | > dt { 20 | display: inline-block; 21 | font-weight: 700; 22 | font-size: 20px; 23 | } 24 | 25 | > dd { 26 | display: inline-block; 27 | margin-left: 10px; 28 | font-size: 14px; 29 | } 30 | 31 | .close { 32 | margin-left: 10px; 33 | } 34 | } 35 | } 36 | 37 | .chart-container { 38 | height: 100%; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/Exchange/KLine/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | getActiveLanguage, 5 | getTranslate, 6 | } from 'react-localize-redux/lib/index'; 7 | 8 | import { subscribeKLine } from '../../../actions/exchangeActions'; 9 | import KLineView from './KLineView'; 10 | 11 | @connect(store => { 12 | return { 13 | translate: getTranslate(store.locale), 14 | currentLanguage: getActiveLanguage(store.locale).code, 15 | symbol: store.exchange.configs.symbol, 16 | latest: store.exchange.latest, 17 | }; 18 | }) 19 | export default class extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.symbol = ''; 24 | } 25 | 26 | componentWillReceiveProps(nextProps) { 27 | if (this.symbol !== nextProps.symbol) { 28 | this.symbol = nextProps.symbol; 29 | this.props.dispatch(subscribeKLine(this.symbol)); 30 | } 31 | } 32 | 33 | componentDidMount() {} 34 | 35 | render() { 36 | return ( 37 | 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/Exchange/Markets/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Table } from 'antd'; 3 | import { connect } from 'react-redux'; 4 | import { getTranslate } from 'react-localize-redux/lib/index'; 5 | import uuid from 'uuid/v1'; 6 | 7 | @connect(store => { 8 | const { markets } = store.exchange; 9 | 10 | return { 11 | translate: getTranslate(store.locale), 12 | markets, 13 | }; 14 | }) 15 | export default class List extends React.Component { 16 | render() { 17 | const { translate, markets } = this.props; 18 | const dataSource = markets.map(row => { 19 | return { 20 | coin: row[0], 21 | price: row[1], 22 | change: row[2], 23 | key: uuid(), 24 | }; 25 | }); 26 | 27 | const columns = [ 28 | { 29 | title: translate('exchange.coin'), 30 | dataIndex: 'coin', 31 | width: '33%', 32 | sorter: (a, b) => a.coin - b.coin, 33 | }, 34 | { 35 | title: translate('exchange.last_price'), 36 | dataIndex: 'price', 37 | width: '34%', 38 | sorter: (a, b) => a.price - b.price, 39 | }, 40 | { 41 | title: translate('exchange.change'), 42 | dataIndex: 'change', 43 | width: '33%', 44 | sorter: (a, b) => a.change - b.change, 45 | render: value => { 46 | return ( 47 | = 0 ? 'color-up' : 'color-down'}> 48 | {value}% 49 | 50 | ); 51 | }, 52 | }, 53 | ]; 54 | 55 | return ( 56 |
57 | 58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/pages/Exchange/Markets/MarketsView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col, Input, Icon } from 'antd'; 3 | import cs from 'classnames'; 4 | 5 | import List from './List'; 6 | 7 | import './MarketsView.css'; 8 | 9 | const MarketsView = ({ 10 | translate, 11 | marketsSymbol, 12 | searchHandle, 13 | changeMarketsSymbol, 14 | }) => { 15 | return ( 16 |
17 |
18 | 19 |
20 | {translate('exchange.markets')} 21 | 22 | 23 | 24 | 25 | 26 | 27 | CNY 28 | 29 | 30 | 31 | 32 | 35 | USDT 36 | 37 | 38 | 39 | 42 | BTC 43 | 44 | 45 | 46 | 49 | ETH 50 | 51 | 52 | 53 | 54 | 55 | {translate('exchange.marked')} 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default MarketsView; 66 | -------------------------------------------------------------------------------- /src/pages/Exchange/Markets/MarketsView.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | border-radius: 5px; 4 | background-color: #1b1e2e; 5 | 6 | .header { 7 | padding-top: 10px; 8 | background-color: #14182a; 9 | box-shadow: 0 3px 6px rgba(0, 0, 0, .1); 10 | } 11 | 12 | .row { 13 | padding: 0 10px; 14 | font-size: 14px; 15 | 16 | .label { 17 | font-size: 16px; 18 | } 19 | 20 | .cny { 21 | color: #4e5b85; 22 | text-align: center; 23 | 24 | .icon { 25 | margin-right: 10px; 26 | } 27 | } 28 | } 29 | 30 | .symbol-wrap { 31 | padding: 0 10px; 32 | margin-top: 20px; 33 | color: #61688a; 34 | 35 | .text-right { 36 | text-align: right; 37 | } 38 | 39 | .star { 40 | margin-right: 5px; 41 | color: #61688a; 42 | } 43 | 44 | .mark { 45 | color: #c7cce6; 46 | } 47 | 48 | .symbol { 49 | display: inline-block; 50 | padding-bottom: 5px; 51 | color: #61688a; 52 | cursor: pointer; 53 | 54 | &.selected { 55 | border-bottom: 1px solid #7a98f7; 56 | color: #7a98f7; 57 | cursor: default; 58 | 59 | .star { 60 | color: #7a98f7; 61 | } 62 | 63 | .mark { 64 | color: #7a98f7; 65 | } 66 | } 67 | } 68 | } 69 | 70 | .table-wrap { 71 | padding-bottom: 10px; 72 | margin: 10px 10px; 73 | } 74 | 75 | :global { 76 | .ant-table-thead tr th { 77 | border-bottom: 1px solid #1f2943; 78 | } 79 | 80 | .ant-table-tbody tr td { 81 | border-bottom: 1px solid #1f2943; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/pages/Exchange/Markets/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getTranslate } from 'react-localize-redux/lib/index'; 4 | 5 | import { 6 | changeSearchWord, 7 | subscribeMarkets, 8 | switchMarkets, 9 | } from '../../../actions/exchangeActions'; 10 | import MarketsView from './MarketsView'; 11 | 12 | @connect(store => { 13 | return { 14 | translate: getTranslate(store.locale), 15 | marketsSymbol: store.exchange.configs.marketsSymbol, 16 | }; 17 | }) 18 | export default class extends React.Component { 19 | componentDidMount() { 20 | this.props.dispatch(subscribeMarkets(this.props.marketsSymbol)); 21 | } 22 | 23 | searchHandle = evt => { 24 | this.props.dispatch(changeSearchWord(evt.target.value)); 25 | }; 26 | 27 | // 切换交易对 28 | changeMarketsSymbol = value => () => { 29 | if (value !== this.props.marketsSymbol) { 30 | console.log(value); 31 | this.props.dispatch(switchMarkets(value)); 32 | } 33 | }; 34 | 35 | render() { 36 | return ( 37 | 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/Exchange/Mscy/Mscy.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from 'antd'; 3 | import './Mscy.css'; 4 | const Mscy = () => { 5 | return ( 6 |
7 |
8 |
9 | 10 | 11 | 币种资料 12 | 13 | 了解更多 14 |
15 |
16 |
17 |

18 | eth{' '} 19 | 20 | 以太坊(Ethereum 21 | 22 |

23 |
24 | 简介 25 |

26 | 以太坊(Ethereum)是下一代密码学账本,可以支持众多的高级功能,包括用户发行货币,智能协议,去中心化的交易和设立去中心化自治组织(DAOs)或去中心化自治公司(DACs)。以太坊并不是把每一单个类型的功能作为特性来特别支持,相反,以太坊包括一个内置的图灵完备的脚本语言,允许通过被称为“合同”的机制来为自己想实现的特性写代码。一个合同就像一个自动的代理,每当接收到一笔交易,合同就会运行特定的一段代码,这段代码能修改合同内部的数据存储或者发送交易。 27 |

28 |
29 |
30 |
31 | 81 |
82 |
83 |
84 |
85 | ); 86 | }; 87 | export default Mscy; 88 | -------------------------------------------------------------------------------- /src/pages/Exchange/Mscy/Mscy.scss: -------------------------------------------------------------------------------- 1 | .coin_detail { 2 | .mod { 3 | border-radius: 3px; 4 | margin-bottom: 10px; 5 | transition: height .15s ease-in-out; 6 | 7 | .mod_hd { 8 | height: 48px; 9 | padding:0 40px 0 15px; 10 | box-shadow: 0 3px 6px rgba(0,0,0,.1); 11 | font-size: 16px; 12 | line-height: 48px; 13 | 14 | .mod_detail { 15 | padding-left: 6px; 16 | } 17 | 18 | > a { 19 | float:right; 20 | color:#7a98f7; 21 | font-size: 14px; 22 | } 23 | 24 | .mod_show_btn { 25 | float: left; 26 | display: -ms-flexbox; 27 | display: flex; 28 | align-items: center; 29 | height: inherit; 30 | cursor: pointer; 31 | -ms-flex-align: center; 32 | -moz-user-select: none; 33 | -webkit-user-select: none; 34 | -ms-user-select: none; 35 | user-select: none; 36 | 37 | } 38 | 39 | } 40 | 41 | .mod_bg { 42 | padding: 10px 30px 30px; 43 | 44 | .left { 45 | float: left; 46 | width: 35%; 47 | padding-bottom: 14px; 48 | 49 | > h3 { 50 | height: 88px; 51 | padding-top: 10px; 52 | border-bottom: 1px solid #1f2943; 53 | font-weight: 400; 54 | 55 | > span { 56 | display: block; 57 | height: 44px; 58 | color:#61688a; 59 | font-style: normal; 60 | font-size: 24px; 61 | text-transform: uppercase; 62 | } 63 | 64 | } 65 | 66 | .in { 67 | > span { 68 | display: block; 69 | padding: 22px 0 10px; 70 | color:#61688a; 71 | } 72 | 73 | > p { 74 | font-size: 13px; 75 | line-height: 24px; 76 | } 77 | } 78 | } 79 | 80 | .right { 81 | float: right; 82 | width: 53%; 83 | font-size: 13px; 84 | 85 | li { 86 | height: 48px; 87 | border-bottom: 1px solid #1f2943; 88 | line-height: 48px; 89 | 90 | > span { 91 | float: left; 92 | width: 30%; 93 | color:#61688a; 94 | overflow: hidden; 95 | text-overflow: ellipsis; 96 | white-space: nowrap; 97 | } 98 | 99 | > p { 100 | width: 70%; 101 | overflow: hidden; 102 | text-overflow: ellipsis; 103 | white-space: nowrap; 104 | } 105 | 106 | } 107 | 108 | } 109 | } 110 | 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/pages/Exchange/Mscy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mscy", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Mscy.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/Exchange/Notice/Notice.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Notice.css'; 3 | const Notice = () => { 4 | return ( 5 |
6 |
7 | 公告 8 |
9 |
10 | 24 |
25 |
26 | ); 27 | }; 28 | export default Notice; 29 | -------------------------------------------------------------------------------- /src/pages/Exchange/Notice/Notice.scss: -------------------------------------------------------------------------------- 1 | .sidebar_notice { 2 | border-radius: 3px; 3 | margin-top: 10px; 4 | margin-bottom: 10px; 5 | background-color:#181b2a; 6 | 7 | .tit { 8 | height: 48px; 9 | padding:0 20px; 10 | box-shadow:0 3px 6px rgba(0,0,0,.1); 11 | font-size: 16px; 12 | line-height: 48px; 13 | 14 | > a { 15 | color:#fff; 16 | } 17 | } 18 | 19 | .in { 20 | padding:0 20px; 21 | 22 | > ul > li { 23 | height: 83px; 24 | padding: 17px 0 10px; 25 | border-top: 1px solid #1f2943; 26 | font-size: 12px; 27 | line-height: 12px; 28 | text-align: right; 29 | cursor: pointer; 30 | 31 | > a { 32 | display:block; 33 | height: 36px; 34 | margin-bottom: 6px; 35 | color:#c7cce6; 36 | overflow: hidden; 37 | line-height: 18px; 38 | text-align: left; 39 | } 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/Exchange/Notice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Notice", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Notice.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/Exchange/Orders/OrdersView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col } from 'antd'; 3 | import uuid from 'uuid/v1'; 4 | 5 | import './OrdersView.css'; 6 | 7 | const SellOrders = ({ lists }) => { 8 | const length = lists.length; 9 | return lists.reverse().map((list, index) => { 10 | return ( 11 |
12 | 13 |
卖 {length - index} 14 | {list[0].toFixed(2)} 15 | {list[1].toFixed(4)} 16 | {list[2].toFixed(4)} 17 | 18 | 19 | ); 20 | }); 21 | }; 22 | 23 | const BuyOrders = ({ lists }) => { 24 | return lists.map((list, index) => { 25 | return ( 26 |
27 | 28 |
买 {index + 1} 29 | {list[0].toFixed(2)} 30 | {list[1].toFixed(4)} 31 | {list[2].toFixed(4)} 32 | 33 | 34 | ); 35 | }); 36 | }; 37 | 38 | const OrdersView = ({ translate, orders, latest, symbol }) => { 39 | return ( 40 |
41 |
42 | {translate('exchange.last_price')} {latest[0]} USDT 43 | ≈ {latest[0]} CNY 44 |
45 |
46 |
47 | 48 |
49 | 价格(USDT) 50 | 51 | 数量( 52 | {symbol}) 53 | 54 | 55 | 累计( 56 | {symbol}) 57 | 58 | 59 | 60 | 61 | 62 |
63 |
64 | 65 |
66 | 67 | ); 68 | }; 69 | export default OrdersView; 70 | -------------------------------------------------------------------------------- /src/pages/Exchange/Orders/OrdersView.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | font-size: 12px; 3 | 4 | dl { 5 | margin: .7em 20px; 6 | } 7 | 8 | dt div { 9 | margin-bottom: 5px; 10 | color: #61688a; 11 | } 12 | 13 | .text-right { 14 | text-align: right; 15 | 16 | div:first-child { 17 | text-align: left; 18 | } 19 | } 20 | 21 | .color-sell { 22 | div:first-child { 23 | color: #ae4e54; 24 | } 25 | } 26 | 27 | .color-buy { 28 | div:first-child { 29 | color: #589065; 30 | } 31 | } 32 | 33 | .divider { 34 | border: 1px solid #1f2943; 35 | margin: 0 8px; 36 | } 37 | 38 | .header { 39 | height: 48px; 40 | padding: 0 20px; 41 | color: #c7cce6; 42 | background-color: #1b1e2e; 43 | box-shadow: 0 3px 6px rgba(0, 0, 0, .1); 44 | font-size: 14px; 45 | line-height: 48px; 46 | 47 | span { 48 | padding-left: 5px; 49 | color: #61688a; 50 | font-size: 12px; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/Exchange/Orders/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getTranslate } from 'react-localize-redux/lib/index'; 4 | 5 | import OrdersView from './OrdersView'; 6 | import { subscribeOrders } from '../../../actions/exchangeActions'; 7 | 8 | @connect(store => { 9 | return { 10 | translate: getTranslate(store.locale), 11 | orders: store.exchange.orders, 12 | latest: store.exchange.latest, 13 | symbol: store.exchange.configs.symbol, 14 | }; 15 | }) 16 | export default class extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.symbol = ''; 21 | } 22 | 23 | componentWillReceiveProps(nextProps) { 24 | if (this.symbol !== nextProps.symbol) { 25 | this.symbol = nextProps.symbol; 26 | this.props.dispatch(subscribeOrders(this.symbol)); 27 | } 28 | } 29 | 30 | render() { 31 | const [symbol] = this.props.symbol.split('_'); 32 | return ( 33 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/Exchange/TradePanel/Limit.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Col, Input, Row, Slider } from 'antd'; 3 | 4 | import './TradePanel.css'; 5 | 6 | export const defaultMarks = { 0: '', 0.25: '', 0.5: '', 0.75: '', 1: '' }; 7 | 8 | // slide组件的分段标记 9 | export function getMarks(max) { 10 | const step = max / 4; 11 | return max > 0 12 | ? { 13 | 0: '', 14 | [step]: '', 15 | [step * 2]: '', 16 | [step * 3]: '', 17 | [step * 4]: '', 18 | } 19 | : defaultMarks; 20 | } 21 | 22 | // slide组件滑块的步长 23 | export function getStep(num) { 24 | let step = null; 25 | if (!num) { 26 | step = null; 27 | } else if (num <= 0.01) { 28 | step = 0.0001; 29 | } else { 30 | step = num / 20; 31 | } 32 | return step; 33 | } 34 | 35 | function floor(num) { 36 | return Math.floor(num * 10000) / 10000; 37 | } 38 | 39 | export default class extends React.Component { 40 | state = { 41 | buyAvailable: 0, // 可购买币数量 42 | buyStep: null, // 滑块步长 43 | buyValue: 0, // slide对应的购买量 44 | buyInputValue: 0, // input对应的购买量 45 | buyMarks: defaultMarks, // slide线段 46 | 47 | sellAvailable: 0, // 与上面一致,反向状态 48 | sellStep: null, 49 | sellValue: 0, 50 | sellInputValue: 0, 51 | sellMarks: defaultMarks, 52 | }; 53 | 54 | componentWillReceiveProps(nextProps) { 55 | const { money, stock, rate } = nextProps; 56 | // 可交易数据不能小于0.00001 57 | const buyAvailable = rate > 0 ? floor(money / rate) : 0; 58 | const sellAvailable = floor(stock); 59 | 60 | // 滑块步长 61 | const buyStep = getStep(buyAvailable); 62 | const sellStep = getStep(sellAvailable); 63 | 64 | this.setState({ 65 | buyAvailable, 66 | buyStep, 67 | buyMarks: getMarks(buyAvailable), 68 | sellAvailable, 69 | sellStep, 70 | sellMarks: getMarks(sellAvailable), 71 | }); 72 | } 73 | 74 | // input组件的onchange 75 | buyInputChangeHandle = evt => { 76 | const value = parseFloat(evt.target.value); 77 | if (isNaN(value)) { 78 | this.setState({ 79 | buyValue: 0, 80 | buyInputValue: '', // 允许用户删除输入框数字,按照0来处理 81 | }); 82 | } else if (value !== this.state.buyValue || value === 0) { 83 | // 输入框内比如1.2000改为1.2没有区别,所以不会出发state变化 84 | this.setState({ 85 | buyValue: value, 86 | buyInputValue: value, 87 | }); 88 | } 89 | }; 90 | 91 | // slide组件的onchange 92 | buyChangeHandle = value => { 93 | if (this.state.buyAvailable > 0) { 94 | this.setState({ 95 | buyValue: value, 96 | buyInputValue: value.toFixed(4), 97 | }); 98 | } 99 | }; 100 | 101 | sellInputChangeHandle = evt => { 102 | const value = parseFloat(evt.target.value); 103 | if (isNaN(value)) { 104 | this.setState({ 105 | sellValue: 0, 106 | sellInputValue: '', 107 | }); 108 | } else if (value !== this.props.sellValue || value === 0) { 109 | this.setState({ 110 | sellValue: value, 111 | sellInputValue: value, 112 | }); 113 | } 114 | }; 115 | 116 | sellChangeHandle = value => { 117 | if (this.state.sellAvailable > 0) { 118 | this.setState({ 119 | sellValue: value, 120 | sellInputValue: value.toFixed(4), 121 | }); 122 | } 123 | }; 124 | 125 | render() { 126 | const { money, stock, left, right, rate } = this.props; 127 | const { 128 | buyAvailable, 129 | buyStep, 130 | buyValue, 131 | buyInputValue, 132 | buyMarks, 133 | sellAvailable, 134 | sellStep, 135 | sellValue, 136 | sellInputValue, 137 | sellMarks, 138 | } = this.state; 139 | 140 | return ( 141 | 142 |
143 |

144 | 145 | 可用 {money} {right} 146 | 147 | 充币 148 |

149 | 150 |
151 |
买入价
152 |
153 | 157 |
158 |
≈ 281.51 CNY
159 |
160 | 161 |
162 |
买入量
163 |
164 | 173 |
174 |
175 | 185 |

186 | 0 {left} 187 | 188 | {buyAvailable.toFixed(4)} {left} 189 | 190 |

191 |

交易额 19752869136.000000 {right}

192 |
193 |
194 | 197 | 198 | 199 | 200 |

201 | 202 | 可用 {stock} {left} 203 | 204 | 充币 205 |

206 | 207 |
208 |
卖出价
209 |
210 | 214 |
215 |
≈ 281.51 CNY
216 |
217 | 218 |
219 |
卖出量
220 |
221 | 230 |
231 |
232 | 242 |

243 | 0 {left} 244 | 245 | {sellAvailable.toFixed(4)} {left} 246 | 247 |

248 |

交易额 19752869136.000000 {right}

249 |
250 |
251 | 252 | 255 | 256 | 257 | ); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/pages/Exchange/TradePanel/Market.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Col, Input, Row, Slider } from 'antd'; 3 | 4 | import { defaultMarks, getMarks } from './Limit'; 5 | import './TradePanel.css'; 6 | 7 | export default class extends React.Component { 8 | state = { 9 | moneyValue: 0, 10 | moneyInputValue: 0, 11 | moneyMarks: defaultMarks, 12 | stockValue: 0, 13 | stockInputValue: 0, 14 | stockMarks: defaultMarks, 15 | }; 16 | 17 | componentWillReceiveProps(nextProps) { 18 | const { money, stock } = nextProps; 19 | this.setState({ 20 | moneyMarks: getMarks(money), 21 | stockMarks: getMarks(stock), 22 | }); 23 | } 24 | 25 | moneyInputChangeHandle = evt => { 26 | const value = parseFloat(evt.target.value); 27 | if (!value) { 28 | this.setState({ 29 | moneyValue: 0, 30 | moneyInputValue: '', 31 | }); 32 | } else if (value !== this.props.moneyValue) { 33 | this.setState({ 34 | moneyValue: value, 35 | moneyInputValue: value, 36 | }); 37 | } 38 | }; 39 | 40 | moneyChangeHandle = value => { 41 | if (this.props.money > 0) { 42 | this.setState({ 43 | moneyValue: value, 44 | moneyInputValue: value.toFixed(2), 45 | }); 46 | } 47 | }; 48 | 49 | stockInputChangeHandle = evt => { 50 | const value = parseFloat(evt.target.value); 51 | if (!value) { 52 | this.setState({ 53 | stockValue: 0, 54 | stockInputValue: '', 55 | }); 56 | } else if (value !== this.props.stockValue) { 57 | this.setState({ 58 | stockValue: value, 59 | stockInputValue: value, 60 | }); 61 | } 62 | }; 63 | 64 | stockChangeHandle = value => { 65 | if (this.props.stock > 0) { 66 | this.setState({ 67 | stockValue: value, 68 | stockInputValue: value.toFixed(2), 69 | }); 70 | } 71 | }; 72 | 73 | render() { 74 | const { money, stock, left, right } = this.props; 75 | const { 76 | moneyValue, 77 | moneyInputValue, 78 | moneyMarks, 79 | stockValue, 80 | stockInputValue, 81 | stockMarks, 82 | } = this.state; 83 | 84 | return ( 85 | 86 | 87 |

88 | 89 | 可用 {money} {right} 90 | 91 | 充币 92 |

93 | 94 |
95 |
买入价
96 |
97 | 105 |
106 |
107 | 108 |
109 |
交易额
110 |
111 | 120 |
121 |
122 | 132 |

133 | 0 {right} 134 | 135 | {money} {right} 136 | 137 |

138 |
139 |
140 | 143 | 144 | 145 | 146 |

147 | 148 | 可用 {stock} {left} 149 | 150 | 充币 151 |

152 | 153 |
154 |
卖出价
155 |
156 | 164 |
165 |
166 | 167 |
168 |
卖出量
169 |
170 | 179 |
180 |
181 | 191 |

192 | 0 {left} 193 | 194 | {stock} {left} 195 | 196 |

197 |
198 |
199 | 200 | 203 | 204 | 205 | ); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/pages/Exchange/TradePanel/TradePanel.scss: -------------------------------------------------------------------------------- 1 | .limit, .market { 2 | justify-content: space-evenly; 3 | 4 | .available { 5 | display: flex; 6 | justify-content: space-between; 7 | padding-bottom: 10px; 8 | border-bottom: 1px solid #1f2943; 9 | font-size: 14px; 10 | 11 | > a { 12 | padding-right: 15px; 13 | color: #7a98f7; 14 | 15 | &:hover { 16 | color: #aabdfa; 17 | } 18 | } 19 | 20 | > span { 21 | text-transform: uppercase; 22 | } 23 | } 24 | 25 | dl { 26 | margin: 0; 27 | 28 | &.price { 29 | min-height: 110px; 30 | } 31 | } 32 | 33 | dt { 34 | display: block; 35 | margin-bottom: 5px; 36 | color: #61688a; 37 | font-size: 14px; 38 | } 39 | 40 | dd { 41 | margin: 0; 42 | 43 | &.holder { 44 | height: 122px; 45 | } 46 | } 47 | 48 | label { 49 | position: relative; 50 | display: block; 51 | 52 | .unit { 53 | position: absolute; 54 | right: 20px; 55 | height: 40px; 56 | color: #61688a; 57 | line-height: 40px; 58 | text-transform: uppercase; 59 | user-select: none; 60 | } 61 | } 62 | 63 | .covert { 64 | height: 24px; 65 | padding-left: 20px; 66 | border-radius: 0 0 3px 3px; 67 | background-color: rgba(78, 91, 133, .4); 68 | line-height: 24px; 69 | user-select: none; 70 | } 71 | 72 | .range { 73 | display: flex; 74 | justify-content: space-between; 75 | margin-top: -20px; 76 | 77 | em { 78 | text-transform: uppercase; 79 | text-decoration: none; 80 | user-select: none; 81 | } 82 | } 83 | 84 | .amount { 85 | padding: 10px 0; 86 | font-size: 16px; 87 | line-height: 36px; 88 | text-transform: uppercase; 89 | } 90 | 91 | .button { 92 | height: 40px; 93 | background-color: #4e5b85; 94 | font-size: 16px; 95 | text-transform: uppercase; 96 | 97 | span { 98 | color: #fff; 99 | } 100 | } 101 | 102 | input[disabled] { 103 | cursor: default; 104 | 105 | &::placeholder { 106 | color: #61688a; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/pages/Exchange/TradePanel/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getTranslate } from 'react-localize-redux/lib/index'; 4 | import { Tabs } from 'antd'; 5 | 6 | import * as user from '../../../actions/userActions'; 7 | import Limit from './Limit'; 8 | import Market from './Market'; 9 | import './TradePanel.css'; 10 | 11 | @connect(store => { 12 | return { 13 | translate: getTranslate(store.locale), 14 | symbol: store.exchange.configs.symbol, 15 | latest: store.exchange.latest, 16 | assets: store.user.assets, 17 | }; 18 | }) 19 | export default class extends React.Component { 20 | render() { 21 | const { symbol, assets, latest } = this.props; 22 | const [left, right] = symbol.split('_'); 23 | return ( 24 |
25 | 26 | 27 | 34 | 35 | 36 | 43 | 44 | 45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/Exchange/common.scss: -------------------------------------------------------------------------------- 1 | // 样式库 2 | 3 | %win-header { 4 | height: 48px; 5 | padding: 0 20px; 6 | color: #c7cce6; 7 | background-color: #1b1e2e; 8 | box-shadow: 0 3px 6px rgba(0, 0, 0, .1); 9 | font-size: 14px; 10 | line-height: 48px; 11 | 12 | > span { 13 | padding-left: 6px; 14 | font-size: 16px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/Exchange/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import * as exchange from '../../actions/exchangeActions'; 5 | import * as user from '../../actions/userActions'; 6 | import GridView from './Grid/GridView'; 7 | 8 | @connect() 9 | export default class extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.symbol = ''; 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | const symbol = nextProps.match.params.symbol; 18 | 19 | if (this.symbol !== symbol) { 20 | this.symbol = symbol; 21 | this.props.dispatch(user.getUserdata()); 22 | this.props.dispatch(exchange.setSymbol(symbol)); 23 | this.props.dispatch(exchange.subscribeLatest(symbol)); 24 | } 25 | } 26 | 27 | // 已经提取了route的参数到redux中,所以不需要重复渲染 28 | shouldComponentUpdate() { 29 | return false; 30 | } 31 | 32 | render() { 33 | return ; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/reducers/exchangeReducer.js: -------------------------------------------------------------------------------- 1 | const initState = (function() { 2 | const markets = [['ENB', 0, 0], ['ECHO', 0, 0]]; 3 | 4 | const orders = { 5 | sell: [], 6 | buy: [], 7 | }; 8 | 9 | // price, change, high, low, vol 10 | const latest = [0, 0, 0, 0, 0]; 11 | // timestamp, direction, price, amount 12 | const deals = [[0, 0, 0, 0]]; 13 | 14 | const trade = { money: 0, stock: 0 }; 15 | 16 | const depth = { 17 | bids: [[0, 0]], // 买1价,买1量 18 | asks: [[0, 0]], // 卖1价,卖1量 19 | }; 20 | 21 | const kline = {}; 22 | 23 | return { 24 | markets, 25 | orders, 26 | latest, 27 | deals, 28 | trade, 29 | depth, 30 | kline, 31 | configs: { 32 | symbol: '', 33 | marketsSymbol: 'usdt', 34 | period: '1day', 35 | searchWord: '', 36 | }, 37 | }; 38 | })(); 39 | 40 | const exchange = (state = initState, action) => { 41 | switch (action.type) { 42 | case 'EXCHANGE.SET_SYMBOL': { 43 | const symbol = action.payload; 44 | const configs = state.configs; 45 | return { ...state, configs: { ...configs, symbol } }; 46 | } 47 | case 'EXCHANGE.CHANGE_SEARCH_WORD': { 48 | const searchWord = action.payload; 49 | const configs = state.configs; 50 | return { ...state, configs: { ...configs, searchWord } }; 51 | } 52 | case 'EXCHANGE.SWITCH_MARKETS': { 53 | const marketsSymbol = action.payload; 54 | const configs = state.configs; 55 | return { ...state, configs: { ...configs, marketsSymbol } }; 56 | } 57 | case 'EXCHANGE.MARKETS_COMPLETE': { 58 | const markets = action.payload; 59 | return { ...state, markets }; 60 | } 61 | case 'EXCHANGE.LATEST_COMPLETE': { 62 | const latest = action.payload; 63 | return { ...state, latest }; 64 | } 65 | case 'EXCHANGE.ORDERS_COMPLETE': { 66 | const orders = action.payload; 67 | return { ...state, orders }; 68 | } 69 | case 'EXCHANGE.DEALS_COMPLETE': { 70 | const deals = action.payload; 71 | return { ...state, deals }; 72 | } 73 | case 'EXCHANGE.DEPTH_COMPLETE': { 74 | const depth = action.payload; 75 | return { ...state, depth }; 76 | } 77 | case 'EXCHANGE.LINE_COMPLETE': { 78 | const kline = action.payload; 79 | return { ...state, kline }; 80 | } 81 | default: 82 | return state; 83 | } 84 | }; 85 | 86 | export default exchange; 87 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { persistReducer } from 'redux-persist'; 3 | import session from 'redux-persist/lib/storage/session'; 4 | import { localizeReducer as locale } from 'react-localize-redux'; 5 | import exchange from './exchangeReducer'; 6 | import user from './userReducer'; 7 | 8 | const appReducer = combineReducers({ 9 | locale, 10 | exchange, 11 | user: persistReducer( 12 | { 13 | key: 'user', 14 | storage: session, 15 | }, 16 | user, 17 | ), 18 | }); 19 | 20 | const rootReducer = (state, action) => { 21 | return appReducer(state, action); 22 | }; 23 | 24 | export default rootReducer; 25 | -------------------------------------------------------------------------------- /src/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | const initState = { 2 | assets: { 3 | usdt: 0, 4 | btc: 0, 5 | eth: 0, 6 | etc: 0, 7 | }, 8 | }; 9 | 10 | const user = (state = initState, action) => { 11 | switch (action.type) { 12 | case 'USER.USER_DATA_COMPLETE': { 13 | const assets = action.payload; 14 | return { ...state, assets }; 15 | } 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default user; 22 | -------------------------------------------------------------------------------- /src/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 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ', 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.', 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/sagas/exchangeActions.js: -------------------------------------------------------------------------------- 1 | import { eventChannel } from 'redux-saga'; 2 | import { put, call, select, takeEvery } from 'redux-saga/effects'; 3 | 4 | import * as conn from '../services/connection'; 5 | import * as actions from '../actions/exchangeActions'; 6 | 7 | function createSocketChannel() { 8 | return eventChannel(emit => { 9 | conn 10 | .on('markets', (symbol, extraArgs, data) => { 11 | return emit(actions.marketsComplete(data.tick.markets)); 12 | }) 13 | .on('latest', (symbol, extraArgs, data) => { 14 | return emit(actions.latestComplete(data.tick.latest)); 15 | }) 16 | .on('orders', (symbol, extraArgs, data) => { 17 | return emit(actions.ordersComplete(data.tick)); 18 | }) 19 | .on('deals', (symbol, extraArgs, data) => { 20 | return emit(actions.dealsComplete(data.tick.deals)); 21 | }) 22 | .on('depth', (symbol, extraArgs, data) => { 23 | return emit(actions.depthComplete(data.tick)); 24 | }) 25 | .on('line', (symbol, extraArgs, data) => { 26 | return emit(actions.klineComplete(data.tick)); 27 | }); 28 | 29 | return () => { 30 | conn.close(); 31 | }; 32 | }); 33 | } 34 | 35 | function* subscribeMarkets(action) { 36 | const symbol = action.payload; 37 | try { 38 | yield call([conn, 'subscribe'], { sub: `market.${symbol}.markets` }); 39 | } catch (e) { 40 | console.log(e); 41 | } 42 | } 43 | 44 | function* switchMarkets(action) { 45 | const symbol = action.payload; 46 | try { 47 | yield call([conn, 'switches'], { sub: `market.${symbol}.markets` }); 48 | } catch (e) { 49 | console.log(e); 50 | } 51 | } 52 | 53 | function* subscribeLatest(action) { 54 | const symbol = action.payload.replace('_', ''); 55 | try { 56 | yield call([conn, 'subscribe'], { sub: `market.${symbol}.latest` }); 57 | } catch (e) { 58 | console.log(e); 59 | } 60 | } 61 | 62 | function* subscribeOrders(action) { 63 | const symbol = action.payload.replace('_', ''); 64 | try { 65 | yield call([conn, 'subscribe'], { sub: `market.${symbol}.orders` }); 66 | } catch (e) { 67 | console.log(e); 68 | } 69 | } 70 | 71 | function* subscribeDeals(action) { 72 | const symbol = action.payload.replace('_', ''); 73 | try { 74 | yield call([conn, 'subscribe'], { sub: `market.${symbol}.deals` }); 75 | } catch (e) { 76 | console.log(e); 77 | } 78 | } 79 | 80 | function* subscribeDepth(action) { 81 | const symbol = action.payload.replace('_', ''); 82 | try { 83 | yield call([conn, 'subscribe'], { sub: `market.${symbol}.depth` }); 84 | } catch (e) { 85 | console.log(e); 86 | } 87 | } 88 | 89 | function* subscribeKLine(action) { 90 | const symbol = action.payload.replace('_', ''); 91 | const { 92 | exchange: { 93 | configs: { period }, 94 | }, 95 | } = yield select(); 96 | 97 | try { 98 | yield call([conn, 'subscribe'], { 99 | sub: `market.${symbol}.kline.${period}`, 100 | }); 101 | } catch (e) { 102 | console.log(e); 103 | } 104 | } 105 | 106 | function* socketResponseHandle(action) { 107 | yield put(action); 108 | } 109 | 110 | export function* watchMarket() { 111 | yield call(conn.createWebSocketConnection); 112 | const socketChannel = yield call(createSocketChannel); 113 | 114 | yield takeEvery(socketChannel, socketResponseHandle); 115 | yield takeEvery('EXCHANGE.SUBSCRIBE_MARKETS', subscribeMarkets); 116 | yield takeEvery('EXCHANGE.SWITCH_MARKETS', switchMarkets); 117 | yield takeEvery('EXCHANGE.SUBSCRIBE_LATEST', subscribeLatest); 118 | yield takeEvery('EXCHANGE.SUBSCRIBE_ORDERS', subscribeOrders); 119 | yield takeEvery('EXCHANGE.SUBSCRIBE_DEALS', subscribeDeals); 120 | yield takeEvery('EXCHANGE.SUBSCRIBE_DEPTH', subscribeDepth); 121 | yield takeEvery('EXCHANGE.SUBSCRIBE_KLINE', subscribeKLine); 122 | } 123 | -------------------------------------------------------------------------------- /src/sagas/index.js: -------------------------------------------------------------------------------- 1 | import { fork, all } from 'redux-saga/effects'; 2 | 3 | import { watchMarket } from './exchangeActions'; 4 | import { watchUser } from './userActions'; 5 | 6 | export default function* root() { 7 | yield all([fork(watchMarket), fork(watchUser)]); 8 | } 9 | -------------------------------------------------------------------------------- /src/sagas/userActions.js: -------------------------------------------------------------------------------- 1 | import { put, call, takeEvery } from 'redux-saga/effects'; 2 | 3 | import * as actions from '../actions/userActions'; 4 | 5 | const API = process.env.REACT_APP_API; 6 | 7 | function* fetchData(action) { 8 | try { 9 | const response = yield call(fetch, API + '/user' + action.payload, { 10 | credentials: 'include', 11 | }); 12 | const json = yield call([response, 'json']); 13 | yield put(actions.userDataComplete(json.data)); 14 | } catch (e) { 15 | console.log(e); 16 | } 17 | } 18 | 19 | export function* watchUser() { 20 | yield takeEvery('USER.GET_USER_DATA', fetchData); 21 | } 22 | -------------------------------------------------------------------------------- /src/services/connection.js: -------------------------------------------------------------------------------- 1 | import findIndex from 'lodash/findIndex'; 2 | import { addEventListener } from 'consolidated-events'; 3 | 4 | import ReconnectingWebSocket from './lib/re-websocket'; 5 | 6 | const subscribers = []; 7 | const handles = new Map(); 8 | let connected = false; 9 | let forceClosed = false; 10 | const WS_URL = process.env.REACT_APP_WS; 11 | let socket; 12 | 13 | export function subscribe(data) { 14 | if (subscribers.some(({ sub }) => sub === data.sub)) { 15 | return; 16 | } 17 | 18 | subscribers.push(data); 19 | if (connected) { 20 | socket.send(JSON.stringify(data)); 21 | } 22 | } 23 | 24 | export function switches(data) { 25 | const [, , switchChannel] = data.sub.split('.'); 26 | for (let subscriber of subscribers) { 27 | const [, , channel] = subscriber.sub.split('.'); 28 | // 在一个channel上,就替换 29 | if (switchChannel === channel) { 30 | unsubscribe(subscriber); 31 | subscribe(data); 32 | return; 33 | } 34 | } 35 | } 36 | 37 | export function unsubscribe(data) { 38 | const index = findIndex(subscribers, { sub: data.sub }); 39 | if (index > -1 && connected) { 40 | subscribers.splice(index, 1); 41 | const unsub = { ...data, unsub: data.sub }; 42 | delete unsub.sub; 43 | socket.send(JSON.stringify(unsub)); 44 | } 45 | } 46 | 47 | export function on(type, fn) { 48 | handles.set(type, fn); 49 | return { on }; 50 | } 51 | 52 | export function off(type) { 53 | handles.delete(type); 54 | } 55 | 56 | export function close() { 57 | if (connected) { 58 | forceClosed = true; 59 | connected = false; 60 | socket.close(); 61 | } 62 | } 63 | 64 | function handle(data) { 65 | const [, symbol, channel, ...extraArgs] = data.ch.split('.'); 66 | if (handles.has(channel)) { 67 | handles.get(channel)(symbol, extraArgs, data); 68 | } 69 | } 70 | 71 | export function createWebSocketConnection() { 72 | socket = new ReconnectingWebSocket(WS_URL); 73 | 74 | const removeOpen = addEventListener(socket, 'open', () => { 75 | for (let sub of subscribers.values()) { 76 | socket.send(JSON.stringify(sub)); 77 | } 78 | connected = true; 79 | }); 80 | 81 | const removeClose = addEventListener(socket, 'close', () => { 82 | if (forceClosed) { 83 | removeOpen(); 84 | removeClose(); 85 | removeMessage(); 86 | socket = null; 87 | return; 88 | } 89 | connected = false; 90 | }); 91 | 92 | const removeMessage = addEventListener(socket, 'message', ({ data }) => { 93 | const msg = JSON.parse(data); 94 | if (msg.tick) { 95 | handle(msg); 96 | } 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /src/services/constants.js: -------------------------------------------------------------------------------- 1 | const IDLE_TIME_OUT = 900; 2 | 3 | const BASE_HOST = ''; 4 | 5 | const COLOR_RAISE = '#589065'; 6 | const COLOR_FALL = '#ae4e54'; 7 | 8 | export default { 9 | IDLE_TIME_OUT, 10 | BASE_HOST, 11 | COLOR_RAISE, 12 | COLOR_FALL, 13 | }; 14 | -------------------------------------------------------------------------------- /src/services/language.js: -------------------------------------------------------------------------------- 1 | import Language from 'lang'; 2 | import { renderToStaticMarkup } from 'react-dom/server'; 3 | import { initialize, addTranslationForLanguage } from 'react-localize-redux'; 4 | 5 | function getParameterByName(name, url) { 6 | if (!url) url = window.location.href; 7 | // eslint-disable-next-line no-useless-escape 8 | name = name.replace(/[\[\]]/g, '\\$&'); 9 | const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); 10 | const results = regex.exec(url); 11 | if (!results) return null; 12 | if (!results[2]) return ''; 13 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 14 | } 15 | 16 | // 这里面的import是异步的 17 | export function initLanguage(store) { 18 | let languagePack, packName; 19 | try { 20 | packName = getParameterByName('lang'); 21 | if (!packName) { 22 | packName = Language.defaultLanguage; 23 | } 24 | languagePack = getLanguage(packName); 25 | } catch (e) { 26 | console.log(e); 27 | packName = Language.defaultLanguage; 28 | languagePack = getLanguage(packName); 29 | } 30 | 31 | store.dispatch( 32 | initialize({ 33 | languages: [ 34 | { name: 'English', code: 'en' }, 35 | { name: 'Chinese', code: 'cn' }, 36 | ], 37 | options: { 38 | renderToStaticMarkup, 39 | missingTranslationCallback: () => { 40 | console.log(111); 41 | }, 42 | defaultLanguage: packName, 43 | }, 44 | }), 45 | ); 46 | 47 | languagePack.then(data => { 48 | store.dispatch(addTranslationForLanguage(data, packName)); 49 | }); 50 | } 51 | 52 | export function getLanguage(key) { 53 | return import('lang/' + key + '.json'); 54 | } 55 | 56 | export function setLanguage(packName, dispatch) { 57 | const languagePack = getLanguage(packName); 58 | languagePack.then(data => { 59 | dispatch(addTranslationForLanguage(data, packName)); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { connectRouter, routerMiddleware } from 'connected-react-router'; 3 | import { persistStore } from 'redux-persist'; 4 | import createSagaMiddleware from 'redux-saga'; 5 | 6 | import { initLanguage } from './services/language'; 7 | import rootReducer from './reducers'; 8 | import rootSaga from './sagas'; 9 | import history from './history'; 10 | 11 | const initialState = {}; 12 | const enhancers = []; 13 | const sagaMiddleware = createSagaMiddleware(); 14 | const middleware = [sagaMiddleware, routerMiddleware(history)]; 15 | 16 | if (process.env.NODE_ENV === 'development') { 17 | const devToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION__; 18 | 19 | if (typeof devToolsExtension === 'function') { 20 | enhancers.push(devToolsExtension()); 21 | } 22 | } 23 | 24 | const composedEnhancers = compose( 25 | applyMiddleware(...middleware), 26 | ...enhancers, 27 | ); 28 | 29 | const store = createStore( 30 | connectRouter(history)(rootReducer), 31 | initialState, 32 | composedEnhancers, 33 | ); 34 | sagaMiddleware.run(rootSaga); 35 | 36 | initLanguage(store); 37 | 38 | const persistor = persistStore(store); 39 | 40 | export { store, persistor }; 41 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | export function getParameterByName(name, url) { 2 | if (!url) url = window.location.href; 3 | // eslint-disable-next-line no-useless-escape 4 | name = name.replace(/[\[\]]/g, '\\$&'); 5 | const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); 6 | const results = regex.exec(url); 7 | if (!results) return null; 8 | if (!results[2]) return ''; 9 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 10 | } 11 | 12 | export function getActiveLanguage(langs) { 13 | for (let i = 0; i < langs.length; i++) { 14 | if (langs[i].active) { 15 | return langs[i].code; 16 | } 17 | } 18 | return 'en'; 19 | } 20 | --------------------------------------------------------------------------------