├── .gitignore ├── README.md ├── config-overrides.js ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── assets │ ├── fonts │ │ └── svg │ │ │ ├── comment.svg │ │ │ ├── goback-backup.svg │ │ │ ├── goback.svg │ │ │ ├── gotop.svg │ │ │ ├── medropout.svg │ │ │ ├── meedit.svg │ │ │ ├── mescore.svg │ │ │ ├── metime.svg │ │ │ ├── meuser.svg │ │ │ ├── nall.svg │ │ │ ├── nask.svg │ │ │ ├── ngood.svg │ │ │ ├── nhome.svg │ │ │ ├── nissue.svg │ │ │ ├── njob.svg │ │ │ ├── nnews.svg │ │ │ ├── notfound.svg │ │ │ ├── nshare.svg │ │ │ ├── nuser.svg │ │ │ ├── operat-del.svg │ │ │ ├── operat-keep-active.svg │ │ │ ├── operat-keep-default.svg │ │ │ ├── page-views.svg │ │ │ ├── send.svg │ │ │ ├── thumbs.svg │ │ │ └── write.svg │ ├── images │ │ ├── cnode-qrcode.png │ │ ├── logo-backup.png │ │ ├── logo.png │ │ └── small-demo.gif │ ├── sass │ │ ├── _layout.scss │ │ ├── _mian.scss │ │ ├── _mixin.scss │ │ ├── common │ │ │ └── _reset.scss │ │ └── global.scss │ └── styles │ │ └── css │ │ └── global.css ├── component │ ├── ArticleItem.jsx │ ├── Header.jsx │ ├── Menu.jsx │ ├── SubHeader.jsx │ ├── common │ │ ├── LazyImage.jsx │ │ ├── MaskPopups.jsx │ │ └── SharedCompt.jsx │ └── plugin │ │ └── touch-loadmore │ │ ├── LoadMore.jsx │ │ ├── load-more.less │ │ ├── react-touch-loader.jsx │ │ └── touch-loader.less ├── index.js ├── script │ ├── firstload.js │ ├── history.js │ ├── reuse.js │ ├── routers.js │ └── utils.js ├── serviceWorker.js ├── store │ ├── actions │ │ ├── ActionArticle.js │ │ ├── ActionNews.js │ │ ├── ActionReply.js │ │ └── ActionTopicList.js │ └── reducer │ │ ├── ReduceTopicList.js │ │ ├── ReducerArticle.js │ │ ├── ReducerNews.js │ │ └── ReducerRoot.js └── view │ ├── App.jsx │ ├── NotFound.jsx │ ├── home │ ├── Article-Comment.jsx │ ├── Article-DecCnt.jsx │ ├── Article-Details.jsx │ ├── Home.jsx │ └── User-Comment.jsx │ ├── info │ └── News.jsx │ ├── personal │ └── Mine.jsx │ └── pose │ └── Issue.jsx └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-cnode-project 2 | 3 | ### React 版 Cnode 社区 4 | 5 | 技术栈: WebPack^3.6、React^16.3、React-Router-Dom^4.2、Antd^3.5、React-Dom^16.3、Redux、React-Redux、Es6、Flex、Sass 6 | 7 | 链接:[在线访问](//liuguanhua.github.io/cnode) 8 | 9 | ![qrcode](https://raw.githubusercontent.com/liuguanhua/react-cnode-project/master/src/assets/images/cnode-qrcode.png) 10 | 11 | ![demo](https://raw.githubusercontent.com/liuguanhua/react-cnode-project/master/src/assets/images/small-demo.gif) 12 | 13 | ### 目录 14 | 15 | ``` 16 | react-cnode-project/ 17 | | 18 | ├──src/ 19 | | 20 | ├──assets/ * 资源文件(图标、图片、样式) 21 | | 22 | ├──component/ * 公共组件 23 | │ 24 | │──script/ * 脚本(Rem适配、Axios请求、路由配置、工具函数) 25 | | 26 | ├──store/ * Redux 27 | │ 28 | ├──view/ * 视图展示页面 29 | │ 30 | │__index.js * 入口文件 31 | │ 32 | │__registerServiceWorker.js * ServiceWorker 33 | | 34 | ├──config-overrides.js * 增加WebPack配置及修改Antd主题颜色 35 | ``` 36 | 37 | ### 功能 38 | 39 | - 登录、登出 40 | - 发表话题 41 | - 个人消息 42 | - 评论、点赞帖子 43 | - 查看用户资料 44 | 45 | ### 运行 46 | 47 | ``` 48 | 下载:git clone https://github.com/liuguanhua/react-cnode-project.git 49 | 进入:cd react-cnode-project 50 | 安装:yarn install 51 | 开发:yarn start 52 | 生产:yarn build 53 | ``` 54 | 55 | 访问 web 页时,需先修改 package.json 文件中 homepage 字段,再进行 yarn build [详见](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#building-for-relative-paths) 56 | 57 | 59 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { 4 | override, 5 | fixBabelImports, 6 | addLessLoader, 7 | addWebpackAlias 8 | } = require('customize-cra') 9 | 10 | const appDirectory = fs.realpathSync(process.cwd()) 11 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath) 12 | 13 | module.exports = { 14 | webpack: override( 15 | fixBabelImports('import', { 16 | libraryName: 'antd', 17 | libraryDirectory: 'es', 18 | style: 'css', 19 | style: true 20 | }), 21 | addLessLoader({ 22 | javascriptEnabled: true, 23 | modifyVars: { 24 | '@primary-color': '#639' 25 | } 26 | }), 27 | addWebpackAlias({ 28 | '@': resolveApp('src'), 29 | '@module': resolveApp('node_modules'), 30 | '@assets': resolveApp('src/assets'), 31 | '@images': resolveApp('src/assets/images'), 32 | '@fonts': resolveApp('src/assets/fonts'), 33 | '@view': resolveApp('src/view'), 34 | '@store': resolveApp('src/store'), 35 | '@component': resolveApp('src/component'), 36 | '@script': resolveApp('src/script') 37 | }), 38 | config => { 39 | //自定义svg loader方式 40 | const len = config.module.rules[2].oneOf.length 41 | config.module.rules[2].oneOf[1].options.plugins.shift() 42 | config.module.rules[2].oneOf[len - 1].exclude.push(/\.svg$/) 43 | config.module.rules[2].oneOf.push({ 44 | test: /\.svg$/, 45 | exclude: /node_modules/, 46 | use: [ 47 | { 48 | loader: 'svg-sprite-loader', 49 | options: { 50 | name: '[name]', 51 | prefixize: true, 52 | regExp: resolveApp('src/assets/fonts/svg/(.*)\\.svg') 53 | } 54 | } 55 | ] 56 | }) 57 | return config 58 | } 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-cnode-project", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://liuguanhua.github.io/cnode/", 6 | "dependencies": { 7 | "antd": "^3.17.0", 8 | "axios": "^0.18.0", 9 | "babel-plugin-import": "^1.11.0", 10 | "classnames": "^2.2.6", 11 | "compass-sass-mixins": "^0.12.7", 12 | "cross-env": "^5.2.0", 13 | "github-markdown-css": "^3.0.1", 14 | "less": "^3.9.0", 15 | "less-loader": "^5.0.0", 16 | "node-sass": "^4.12.0", 17 | "npm-run-all": "^4.1.5", 18 | "react": "^16.8.6", 19 | "react-app-rewired": "^2.1.3", 20 | "react-dom": "^16.8.6", 21 | "react-lazy-image": "^1.1.0", 22 | "react-loadable": "^5.5.0", 23 | "react-redux": "^7.0.3", 24 | "react-router-dom": "^5.0.0", 25 | "react-scripts": "^3.0.0", 26 | "redux": "^4.0.1", 27 | "redux-thunk": "^2.3.0", 28 | "store": "^2.0.12", 29 | "svg-sprite-loader": "^4.1.6" 30 | }, 31 | "scripts": { 32 | "clean": "rimraf build", 33 | "react-app-start": "cross-env PORT=8888 react-app-rewired start", 34 | "start": "rimraf build && npm-run-all -p react-app-start", 35 | "build": "react-app-rewired build", 36 | "test": "react-app-rewired test", 37 | "eject": "react-scripts eject" 38 | }, 39 | "eslintConfig": { 40 | "extends": "react-app" 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "devDependencies": { 55 | "babel-jest": "24.7.1", 56 | "customize-cra": "^0.2.12" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuguanhua/react-cnode-project/c3a2f1e852c0c238c18f79aa870363af6b2e71bc/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 31 | react-cnode 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /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": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/comment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/goback-backup.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/goback.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/gotop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/medropout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/meedit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/mescore.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/metime.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/meuser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/nall.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/nask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/ngood.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/nhome.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/nissue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/njob.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/nnews.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/notfound.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/nshare.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/nuser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/operat-del.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/operat-keep-active.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/operat-keep-default.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/page-views.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/send.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/thumbs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/svg/write.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/cnode-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuguanhua/react-cnode-project/c3a2f1e852c0c238c18f79aa870363af6b2e71bc/src/assets/images/cnode-qrcode.png -------------------------------------------------------------------------------- /src/assets/images/logo-backup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuguanhua/react-cnode-project/c3a2f1e852c0c238c18f79aa870363af6b2e71bc/src/assets/images/logo-backup.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuguanhua/react-cnode-project/c3a2f1e852c0c238c18f79aa870363af6b2e71bc/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/small-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuguanhua/react-cnode-project/c3a2f1e852c0c238c18f79aa870363af6b2e71bc/src/assets/images/small-demo.gif -------------------------------------------------------------------------------- /src/assets/sass/_layout.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100%; 3 | padding: 1.9rem 0 2.7rem; 4 | } 5 | 6 | .container-top { 7 | padding-top: 2rem; 8 | } 9 | 10 | .container-bottom { 11 | padding-bottom: 2.7rem; 12 | } 13 | 14 | [data-layout-flex='0'] { 15 | @include flex(none); 16 | } 17 | 18 | [layout], 19 | [data-layout='layout'] { 20 | @include display-flex; 21 | } 22 | 23 | [data-layout-wrap], 24 | [flex] { 25 | @include flex(1); 26 | } 27 | 28 | [data-layout-wrap], 29 | [layout-wrap] { 30 | @include flex-wrap(wrap); 31 | } 32 | 33 | [data-layout='row'], 34 | [layout='row'] { 35 | @include flex-direction(row); 36 | } 37 | 38 | [data-layout='column'], 39 | [layout='column'] { 40 | @include flex-direction(column); 41 | } 42 | 43 | [data-layout-align='start start'], 44 | [data-layout-align='start center'], 45 | [data-layout-align='start end'] [layout-align='start start'], 46 | [layout-align='start center'], 47 | [layout-align='start end'] { 48 | @include justify-content(flex-start); 49 | } 50 | 51 | [data-layout-align='center start'], 52 | [data-layout-align='center center'], 53 | [data-layout-align='center end'], 54 | [layout-align='center start'], 55 | [layout-align='center center'], 56 | [layout-align='center end'] { 57 | @include justify-content(center); 58 | } 59 | 60 | [data-layout-align='end start'], 61 | [data-layout-align='end center'], 62 | [data-layout-align='end end'] [layout-align='end start'], 63 | [layout-align='end center'], 64 | [layout-align='end end'] { 65 | @include justify-content(flex-end); 66 | } 67 | 68 | [data-layout-align='space-between start'], 69 | [data-layout-align='space-between center'], 70 | [data-layout-align='space-between end'], 71 | [layout-align='space-between start'], 72 | [layout-align='space-between center'], 73 | [layout-align='space-between end'] { 74 | @include justify-content(space-between); 75 | } 76 | 77 | [data-layout-align='space-arround start'], 78 | [data-layout-align='space-arround center'], 79 | [data-layout-align='space-arround end'] [layout-align='space-arround start'], 80 | [layout-align='space-arround center'], 81 | [layout-align='space-arround end'] { 82 | @include justify-content(space-arround); 83 | } 84 | 85 | [data-layout-align='start start'], 86 | [data-layout-align='center start'], 87 | [data-layout-align='end start'], 88 | [data-layout-align='space-between start'], 89 | [data-layout-align='space-arround start'] [layout-align='start start'], 90 | [layout-align='center start'], 91 | [layout-align='end start'], 92 | [layout-align='space-between start'], 93 | [layout-align='space-arround start'] { 94 | @include align-items(flex-start); 95 | } 96 | 97 | [data-layout-align='start center'], 98 | [data-layout-align='center center'], 99 | [data-layout-align='end center'], 100 | [data-layout-align='space-between center'], 101 | [data-layout-align='space-arround center'] [layout-align='start center'], 102 | [layout-align='center center'], 103 | [layout-align='end center'], 104 | [layout-align='space-between center'], 105 | [layout-align='space-arround center'] { 106 | @include align-items(center); 107 | } 108 | 109 | [data-layout-align='start end'], 110 | [data-layout-align='center end'], 111 | [data-layout-align='end end'], 112 | [data-layout-align='space-between end'], 113 | [data-layout-align='space-arround end'] [layout-align='start end'], 114 | [layout-align='center end'], 115 | [layout-align='end end'], 116 | [layout-align='space-between end'], 117 | [layout-align='space-arround end'] { 118 | @include align-items(flex-end); 119 | } 120 | -------------------------------------------------------------------------------- /src/assets/sass/_mian.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | /* ========== public ========== */ 3 | 4 | .fl { 5 | @include float; 6 | } 7 | 8 | .fr { 9 | @include float-right; 10 | } 11 | 12 | .pr { 13 | position: relative; 14 | } 15 | 16 | .pa { 17 | position: absolute; 18 | } 19 | 20 | .pf { 21 | position: fixed; 22 | } 23 | 24 | .pl { 25 | padding-left: $pd; 26 | } 27 | 28 | .t0 { 29 | top: 0; 30 | } 31 | 32 | .b0 { 33 | bottom: 0; 34 | } 35 | 36 | .l0 { 37 | left: 0; 38 | } 39 | 40 | .r0 { 41 | right: 0; 42 | } 43 | 44 | .tl0 { 45 | top: 0; 46 | left: 0; 47 | } 48 | 49 | .bl0 { 50 | bottom: 0; 51 | left: 0; 52 | } 53 | 54 | .lr0 { 55 | right: 0; 56 | left: 0; 57 | margin: auto; 58 | } 59 | 60 | .tb0 { 61 | top: 0; 62 | bottom: 0; 63 | } 64 | 65 | .tblr { 66 | @extend %tblr; 67 | } 68 | 69 | .z1 { 70 | z-index: 1; 71 | } 72 | 73 | .z3 { 74 | z-index: 3; 75 | } 76 | 77 | .z5 { 78 | z-index: 5; 79 | } 80 | 81 | .wh100 { 82 | @include plwh(100%); 83 | } 84 | 85 | [hide], 86 | .hide { 87 | @extend %hide; 88 | } 89 | 90 | [show] { 91 | @extend %show; 92 | } 93 | 94 | .ilbk, 95 | .icons, 96 | .icon, 97 | .svg, 98 | [ilbk] { 99 | @include inline-block; 100 | } 101 | 102 | .mgt { 103 | margin-top: $pd; 104 | } 105 | 106 | .rdir-mgt { 107 | margin-top: -$pd; 108 | } 109 | 110 | .mgb { 111 | margin-bottom: $pd; 112 | } 113 | 114 | .mgl { 115 | margin-left: $pd; 116 | } 117 | 118 | .mgr { 119 | margin-right: $pd; 120 | } 121 | 122 | .mgtb { 123 | margin: $pd 0; 124 | } 125 | 126 | .mglr { 127 | margin: 0 $pd; 128 | } 129 | 130 | .mgt { 131 | margin-top: $pd; 132 | } 133 | 134 | .pd { 135 | padding: $pd; 136 | } 137 | 138 | .pdtb { 139 | padding: $pd 0; 140 | } 141 | 142 | .pdtr { 143 | padding-top: $pd; 144 | padding-right: $pd; 145 | } 146 | 147 | .pdlr { 148 | padding: 0 $pd; 149 | } 150 | 151 | .pdtl { 152 | padding-top: $pd; 153 | padding-left: $pd; 154 | } 155 | 156 | .pdlb { 157 | padding-bottom: $pd; 158 | padding-left: $pd; 159 | } 160 | 161 | .pdtbl { 162 | padding: $pd 0 $pd $pd; 163 | } 164 | 165 | .pdl { 166 | padding-left: $pd; 167 | } 168 | 169 | .pdt { 170 | padding-top: $pd; 171 | } 172 | 173 | .pdr { 174 | padding-right: $pd; 175 | } 176 | 177 | .pdb { 178 | padding-bottom: $pd; 179 | } 180 | 181 | .w-half { 182 | width: 50%; 183 | } 184 | 185 | .tc { 186 | @extend %tc; 187 | } 188 | 189 | .tr { 190 | @extend %tr; 191 | } 192 | 193 | .tl { 194 | @extend %tl; 195 | } 196 | 197 | .ys-white { 198 | @extend %ft-white; 199 | } 200 | 201 | .st-font { 202 | @extend %ft-sfm; 203 | } 204 | 205 | %fz8, 206 | .fz8 { 207 | font-size: 0.8rem; 208 | } 209 | 210 | [ellipsis], 211 | [data-ellipsis] { 212 | @include ellipsis; 213 | } 214 | 215 | [ft-color] { 216 | @extend %ft-color; 217 | } 218 | 219 | [ft-white], 220 | .ft-white { 221 | @extend %ft-white; 222 | } 223 | 224 | .ft-sgrey { 225 | @extend %ft-sgrey; 226 | } 227 | 228 | .ft-qgrey { 229 | @extend %ft-qgrey; 230 | } 231 | 232 | .ft-grey { 233 | @extend %ft-grey; 234 | } 235 | 236 | .ft-orange { 237 | @extend %ft-orange; 238 | } 239 | 240 | .ft-color { 241 | @extend %ft-color; 242 | } 243 | 244 | .ft-purple { 245 | color: #00f0ff; 246 | } 247 | 248 | .w100 { 249 | width: 100%; 250 | } 251 | 252 | .h100 { 253 | height: 100%; 254 | } 255 | 256 | .ft-bold { 257 | font-weight: 900; 258 | } 259 | 260 | .ft-normal { 261 | font-weight: normal; 262 | } 263 | 264 | [ft-hidden] { 265 | overflow: hidden; 266 | text-overflow: ellipsis; 267 | display: -webkit-box; 268 | -webkit-box-orient: vertical; 269 | } 270 | 271 | .vam { 272 | vertical-align: middle; 273 | } 274 | 275 | .img-tc { 276 | @extend %show; 277 | max-width: 100%; 278 | margin-left: auto; 279 | margin-right: auto; 280 | } 281 | 282 | .dp-flex { 283 | @include display-flex; 284 | } 285 | 286 | [bg-color] { 287 | @extend %bg-color; 288 | } 289 | 290 | .bg-sgrey { 291 | @extend %bg-sgrey; 292 | } 293 | 294 | .bg-qgrey { 295 | @extend %bg-qgrey; 296 | } 297 | 298 | .bg-grey { 299 | @extend %bg-grey; 300 | } 301 | 302 | .bg-white { 303 | @extend %bg-white; 304 | } 305 | 306 | .bg-orange { 307 | @extend %bg-orange; 308 | } 309 | 310 | .bg-color { 311 | @extend %bg-color; 312 | } 313 | 314 | .bg-black { 315 | @extend %bg-black; 316 | } 317 | 318 | .bg-qblack { 319 | @extend %bg-qblack; 320 | } 321 | 322 | .bg-transparent { 323 | background-color: rgba($ft-black, .5); 324 | } 325 | 326 | .bg-color-none { 327 | background-color: transparent; 328 | } 329 | 330 | .bdr-half { 331 | @include border-radius(50%); 332 | } 333 | 334 | /* ========== flex ========== */ 335 | 336 | .numFt { 337 | font-family: 'Georgia'; //好看的数字字体 338 | } 339 | 340 | .bd-radius { 341 | border-radius: .3rem; 342 | } 343 | 344 | .bd-none { 345 | border: none; 346 | } 347 | 348 | .small-font { 349 | font-size: .6rem; 350 | } 351 | 352 | .tab-wrap { 353 | position: fixed; 354 | top: 0; 355 | left: 0; 356 | display: table; 357 | width: 100%; 358 | height: 100%; 359 | } 360 | 361 | .tab-wrap-cell { 362 | display: table-cell; 363 | vertical-align: middle; 364 | } 365 | 366 | %mask-tm { 367 | position: fixed; 368 | z-index: 11; 369 | top: 0; 370 | left: 0; 371 | width: 100%; 372 | height: 100%; 373 | background-color: #000; 374 | background-color: rgba(0, 0, 0, .5); 375 | } 376 | 377 | .mask-layer { 378 | @extend %mask-tm; 379 | } 380 | 381 | .mask-page { 382 | z-index: 1; 383 | @extend %mask-tm; 384 | } 385 | -------------------------------------------------------------------------------- /src/assets/sass/_mixin.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | /*========== 主体颜色 ==========*/ 3 | 4 | $pd: 0.4rem; 5 | $ft-color: #639; 6 | $ft-white: #fff; 7 | $ft-black: #000; 8 | $ft-qblack: #333; 9 | $ft-orange: #fcd667; 10 | $ft-grey: #909ba5; 11 | $ft-sgrey: #b6b6b6; 12 | $ft-qgrey: #f3f3f3; 13 | $ft-golden: #baa16c; 14 | $ft-blue: #3068b5; 15 | $ft-sblue: #123b77; 16 | $hv-color: #5f8c02; 17 | $mg10: 10px; 18 | $cheek: 1px solid $ft-grey; 19 | /*========== 网页字体 ==========*/ 20 | 21 | %ft-wfm { 22 | font-family: "Helvetica Neue", Helvetica, STHeiTi, Arial, sans-serif; 23 | } 24 | 25 | %ft-sfm { 26 | font-family: "simsun", Helvetica Neue, Helvetica, STHeiTi, Arial, sans-serif; 27 | } 28 | 29 | /* ========== 字体颜色 ========== */ 30 | 31 | %ft-color { 32 | color: $ft-color; 33 | } 34 | 35 | %ft-white { 36 | color: $ft-white; 37 | } 38 | 39 | %ft-black { 40 | color: $ft-black; 41 | } 42 | 43 | %ft-qblack { 44 | color: $ft-qblack; 45 | } 46 | 47 | %ft-grey { 48 | color: $ft-grey; 49 | } 50 | 51 | %ft-qgrey { 52 | color: $ft-qgrey; 53 | } 54 | 55 | %ft-sgrey { 56 | color: $ft-sgrey; 57 | } 58 | 59 | %ft-sblue { 60 | color: $ft-sblue; 61 | } 62 | 63 | %ft-golden { 64 | color: $ft-golden; 65 | } 66 | 67 | %ft-orange { 68 | color: $ft-orange; 69 | } 70 | 71 | %hv-ft-color { 72 | color: $ft-color; 73 | } 74 | 75 | /* ========== 字体大小 ========== */ 76 | 77 | %ft-big { 78 | font-size: 16px; 79 | } 80 | 81 | %ft-small { 82 | font-size: 12px; 83 | line-height: 22px; 84 | } 85 | 86 | /* ========== 文本位置 ========== */ 87 | 88 | %tc { 89 | text-align: center; 90 | } 91 | 92 | %tr { 93 | text-align: right; 94 | } 95 | 96 | %tl { 97 | text-align: left; 98 | } 99 | 100 | /* ========== 背景颜色 ========== */ 101 | 102 | %bg-black { 103 | background: $ft-black; 104 | } 105 | 106 | %bg-qblack { 107 | background: $ft-qblack; 108 | } 109 | 110 | %bg-grey { 111 | background: $ft-grey; 112 | } 113 | 114 | %bg-sgrey { 115 | background: $ft-sgrey; 116 | } 117 | 118 | %bg-qgrey { 119 | background: $ft-qgrey; 120 | } 121 | 122 | %bg-white { 123 | background-color: $ft-white; 124 | } 125 | 126 | %bg-color { 127 | background-color: $ft-color; 128 | } 129 | 130 | %bg-orange { 131 | background-color: $ft-orange; 132 | } 133 | 134 | %bg-blue { 135 | background-color: $ft-blue; 136 | } 137 | 138 | %bg-hv { 139 | background-color: $hv-color; 140 | } 141 | 142 | %bd-cheek { 143 | border: $cheek; 144 | } 145 | 146 | %fslse { 147 | content: ""; 148 | } 149 | 150 | @mixin border($bdcolor) { 151 | border: 1px solid $bdcolor; 152 | } 153 | 154 | @mixin plborder($tblr, $bdcolor) { 155 | border-#{$tblr}: 1px solid $bdcolor; 156 | } 157 | 158 | @mixin plmargin($tblr, $mgpx) { 159 | margin-#{$tblr}: $mgpx; 160 | } 161 | 162 | @mixin margin($mg) { 163 | margin: $mg; 164 | } 165 | 166 | @mixin plpadding($tblr, $pdpx) { 167 | padding-#{$tblr}: $pdpx; 168 | } 169 | 170 | @mixin plhlh($hlh) { 171 | height: $hlh; 172 | line-height: $hlh; 173 | } 174 | 175 | %show { 176 | display: block; 177 | } 178 | 179 | %hide { 180 | display: none; 181 | } 182 | 183 | %tblr { 184 | position: absolute; 185 | top: 0; 186 | bottom: 0; 187 | left: 0; 188 | right: 0; 189 | margin: auto; 190 | } 191 | 192 | /* ========== clear ========== */ 193 | 194 | @mixin clear { 195 | *zoom: 1; 196 | &:before, 197 | &:after { 198 | content: ""; 199 | display: table; 200 | height: 0; 201 | } 202 | &:after { 203 | clear: both; 204 | } 205 | } 206 | 207 | %clear { 208 | @include clear; 209 | } 210 | 211 | /* ========== opacity ========== */ 212 | 213 | @mixin plwh($wh) { 214 | width: $wh; 215 | height: $wh; 216 | } 217 | 218 | @mixin wh($w, $h) { 219 | width: $w; 220 | height: $h; 221 | } 222 | 223 | @mixin plwhr($wh) { 224 | width: $wh; 225 | height: $wh; // @include border-radius(50%); 226 | } 227 | -------------------------------------------------------------------------------- /src/assets/sass/common/_reset.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | html { 3 | @extend %ft-black; 4 | -webkit-text-size-adjust: 100%; 5 | -ms-text-size-adjust: 100%; 6 | /* 禁用iPhone中Safari的字号自动调整 */ 7 | text-rendering: optimizelegibility; 8 | max-width: 768px; 9 | margin: auto; 10 | @extend %bg-qgrey; 11 | touch-action: none; 12 | } 13 | 14 | body { 15 | /*解决上下拉动滚动条时卡顿、慢*/ 16 | -webkit-overflow-scrolling: touch; 17 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 18 | overflow-scrolling: touch; 19 | @extend %bg-qgrey; 20 | } 21 | 22 | *, 23 | *:after, 24 | *:before { 25 | box-sizing: border-box; 26 | } 27 | 28 | article, 29 | aside, 30 | blockquote, 31 | body, 32 | button, 33 | code, 34 | dd, 35 | details, 36 | dl, 37 | dt, 38 | fieldset, 39 | figcaption, 40 | figure, 41 | footer, 42 | form, 43 | h1, 44 | h2, 45 | h3, 46 | h4, 47 | h5, 48 | h6, 49 | header, 50 | hr, 51 | input, 52 | legend, 53 | li, 54 | menu, 55 | nav, 56 | ol, 57 | p, 58 | pre, 59 | section, 60 | td, 61 | textarea, 62 | th, 63 | ul { 64 | margin: 0; 65 | padding: 0; 66 | } 67 | 68 | article, 69 | aside, 70 | details, 71 | figcaption, 72 | figure, 73 | footer, 74 | header, 75 | menu, 76 | nav, 77 | section { 78 | display: block; 79 | } 80 | 81 | audio, 82 | canvas, 83 | video { 84 | display: inline-block; 85 | } 86 | 87 | a, 88 | body, 89 | button, 90 | html, 91 | input, 92 | select, 93 | textarea { 94 | font: 0.7rem/0.8rem 'Helvetica Neue', Helvetica, STHeiTi, Arial, sans-serif; 95 | outline: none; 96 | } 97 | 98 | table { 99 | border-collapse: collapse; 100 | border-spacing: 0; 101 | } 102 | 103 | fieldset, 104 | iframe, 105 | img { 106 | border: 0; 107 | } 108 | 109 | abbr, 110 | acronym { 111 | border-bottom: 1px dotted; 112 | font-variant: normal; 113 | } 114 | 115 | abbr { 116 | cursor: help; 117 | } 118 | 119 | address, 120 | caption, 121 | cite, 122 | code, 123 | dfn, 124 | em, 125 | th, 126 | var { 127 | font-style: normal; 128 | } 129 | 130 | ol, 131 | ul { 132 | list-style: none; 133 | } 134 | 135 | caption, 136 | th { 137 | text-align: left; 138 | } 139 | 140 | img { 141 | vertical-align: middle; 142 | max-width: 100%; 143 | height: auto; 144 | width: auto\9; 145 | /* ie8 */ 146 | -ms-interpolation-mode: bicubic; 147 | /*为了照顾ie图片缩放失真*/ 148 | } 149 | 150 | em { 151 | font-style: normal; 152 | } 153 | 154 | a { 155 | border: none; 156 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 157 | -webkit-tap-highlight-color: transparent; 158 | outline: none; 159 | @extend %ft-black; 160 | text-decoration: none; 161 | } 162 | 163 | //IE下 164 | a:focus { 165 | outline: none; 166 | } 167 | 168 | //FF下 169 | a.hidefocus { 170 | outline: none; 171 | } 172 | 173 | a:hover { 174 | text-decoration: none; 175 | } 176 | 177 | mark { 178 | background: #fffdd1; 179 | border-bottom: 1px solid #ffedce; 180 | padding: 2px; 181 | margin: 0 5px; 182 | } 183 | 184 | code, 185 | pre, 186 | pre tt { 187 | font-family: Courier, 'Courier New', monospace; 188 | } 189 | 190 | figcaption, 191 | small { 192 | font-size: 14px; 193 | color: #888; 194 | } 195 | 196 | b, 197 | strong { 198 | font-weight: 700; 199 | } 200 | 201 | //[draggable]{cursor:move;} 202 | /*========== headline ==========*/ 203 | 204 | h1, 205 | h2, 206 | h3, 207 | h4, 208 | h5, 209 | h6 { 210 | font-weight: normal; 211 | } 212 | 213 | h1 { 214 | font-size: 1.2rem; 215 | line-height: 1.6rem; 216 | } 217 | 218 | h2 { 219 | font-size: 0.85rem; 220 | line-height: 1.4rem; 221 | } 222 | 223 | h3 { 224 | font-size: 0.75rem; 225 | line-height: 1.2rem; 226 | } 227 | 228 | h4 { 229 | font-size: 0.6rem; 230 | line-height: 0.8rem; 231 | } 232 | 233 | h5 { 234 | font-size: 0.4rem; 235 | line-height: 0.6rem; 236 | } 237 | 238 | h6 { 239 | font-size: 0.2rem; 240 | line-height: 0.4rem; 241 | } 242 | 243 | /*========== clearfix ==========*/ 244 | 245 | .clearfix { 246 | zoom: 1; 247 | display: inline-table; 248 | } 249 | 250 | /* Hides from IE-mac \*/ 251 | 252 | [clearfix]:after, 253 | [clearfix]:before { 254 | content: ''; 255 | display: table; 256 | } 257 | 258 | [clearfix]:after { 259 | clear: both; 260 | } 261 | 262 | * html [clearfix] { 263 | height: 1%; 264 | } 265 | 266 | [clearfix] { 267 | // display: block; 268 | } 269 | 270 | /* End hide from IE-mac */ 271 | 272 | * + html [clearfix] { 273 | min-height: 1%; 274 | } 275 | 276 | /* IE7 */ 277 | 278 | .clearFd { 279 | clear: both; 280 | } 281 | 282 | /*========== form ==========*/ 283 | 284 | input[type='checkbox'] { 285 | vertical-align: middle; 286 | &:checked { 287 | appearance: none; 288 | } 289 | } 290 | 291 | button, 292 | input[type='button'], 293 | input[type='submit'], 294 | input[type='checkbox'], 295 | input[type='reset'] { 296 | cursor: pointer; 297 | } 298 | 299 | input[type='text']:focus, 300 | input[type='password']:focus, 301 | textarea:focus, 302 | input[type='email']:focus, 303 | input[type='number']:focus, 304 | input[type='search']:focus, 305 | button:focus, 306 | input[type='reset']:focus, 307 | input[type='submit']:focus, 308 | input[type='button']:focus { 309 | outline: none; 310 | } 311 | 312 | input[type='text']:focus, 313 | input[type='password']:focus, 314 | textarea:focus, 315 | input[type='email']:focus, 316 | input[type='number']:focus, 317 | input[type='search']:focus { 318 | // @include box-shadow(0 0 .2rem $ft-sgrey); 319 | } 320 | 321 | button::-moz-focus-inner, 322 | input::-moz-focus-inner { 323 | padding: 0; 324 | border: 0; 325 | } 326 | 327 | textarea { 328 | resize: none; 329 | overflow: auto; 330 | @include box-shadow(0 0 0 rgba(0, 0, 0, 0)); 331 | -webkit-appearance: none; 332 | } 333 | 334 | /* 去除iPhone中默认的input样式 */ 335 | 336 | input { 337 | resize: none; 338 | } 339 | 340 | button { 341 | border: none; 342 | } 343 | 344 | // -webkit-user-select: none;/*禁止复制、选中文本*/ 345 | // -moz-user-select: none; 346 | // -khtml-user-select: none; 347 | // user-select: none; 348 | button, 349 | input { 350 | outline: none; // 351 | appearance: none; //去掉ios input 默认圆角 352 | -webkit-appearance: none; 353 | border-radius: 0; //去掉ios button 默认圆角 354 | -webkit-tap-highlight-color: transparent; //去掉ios在触发的时候,input会有一个灰色的背景色块 355 | -webkit-touch-callout: none; 356 | /*长时间按住页面出现闪退*/ 357 | } 358 | 359 | /*========== Delete button under Firefox default style ==========*/ 360 | 361 | input[type='reset']::-moz-focus-inner, 362 | input[type='button']::-moz-focus-inner, 363 | input[type='submit']::-moz-focus-inner, 364 | input[type='file'] > input[type='button']::-moz-focus-inner { 365 | border: none; 366 | padding: 0; 367 | } 368 | -------------------------------------------------------------------------------- /src/assets/sass/global.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @import 'mixin'; //mixin 3 | @import './node_modules/compass-sass-mixins/lib/compass'; 4 | @import 'common/reset'; //重置浏览器页面样式 5 | @import 'layout'; //布局 6 | @import 'mian'; //主体样式 7 | #root { 8 | height: 100%; 9 | } 10 | 11 | a:hover { 12 | color: $ft-color; 13 | } 14 | 15 | .header { 16 | .head-logo { 17 | padding: $pd * 1.5 0; 18 | font-size: 0.8rem; 19 | } 20 | } 21 | 22 | .tab-nav-list, 23 | .footer-menu { 24 | li { 25 | text-align: center; 26 | } 27 | a { 28 | display: block; 29 | padding: $pd 0; 30 | } 31 | } 32 | 33 | .tab-nav-list { 34 | li { 35 | width: 20%; 36 | } 37 | } 38 | 39 | .nav-top-fixed { 40 | box-shadow: 0 0 5px rgba($ft-grey, 0.1); 41 | } 42 | 43 | .footer-menu { 44 | width: 100%; // background-image: linear-gradient(to bottom , #1f3c29,#0c0e34); 45 | background-color: #fff; 46 | box-shadow: 0 0 10px rgba($ft-grey, 0.05); 47 | li { 48 | width: 25%; 49 | } 50 | a { 51 | padding: $pd/2 0; 52 | } 53 | .svg { 54 | @include plwh(1.5rem); 55 | } 56 | .svg-nav-active { 57 | display: none; 58 | } 59 | .nav-selected { 60 | .svg-nav-active { 61 | display: inline-block; 62 | } 63 | .svg-nav-default { 64 | display: none; 65 | } 66 | p { 67 | color: $ft-color; 68 | } 69 | .news-number { 70 | background-color: $ft-color; 71 | } 72 | } 73 | .news-number { 74 | @include plwh(1rem); 75 | right: 1.2rem; 76 | top: 0; 77 | } 78 | } 79 | 80 | .photo { 81 | @include plwh(10rem); 82 | } 83 | 84 | .scroll-wrap { 85 | padding-top: 1.7rem; 86 | } 87 | 88 | .iscroll-wrapper.loadmore-wrap { 89 | top: 3.6rem; 90 | bottom: 2.6rem; 91 | } 92 | 93 | .loadmore-content { 94 | padding: 1.6rem 0 2.7rem; 95 | } 96 | 97 | .article-item { 98 | padding: $pd; 99 | background-color: #fff; 100 | @at-root .author-avatar { 101 | @include plwh(4rem); 102 | } 103 | @at-root .small-avatar-wrap { 104 | @include plwh(2.5rem); 105 | } 106 | @at-root .small-author-avatar { 107 | @include plwh(2rem); 108 | } 109 | .title-type { 110 | padding: 0 $pd / 2; 111 | } 112 | .svg { 113 | @include plwh(1rem); 114 | } 115 | } 116 | 117 | .ant-back-top { 118 | @include plwh(2.5rem); 119 | position: fixed; 120 | bottom: 4rem; 121 | right: 1rem; 122 | border-radius: 50%; 123 | .svg-gotop { 124 | @include plwh(1.8rem); 125 | } 126 | } 127 | 128 | //动画 129 | .view-main { 130 | transition: 0.5s; 131 | } 132 | 133 | .hideLeft { 134 | opacity: 0.75; 135 | transform: translate3d(-100%, 0, 0); 136 | transition: 0.5s; 137 | } 138 | 139 | .hideRight { 140 | opacity: 0.75; 141 | transform: translate3d(100%, 0, 0); 142 | transition: 0.5s; 143 | } 144 | 145 | .example-enter { 146 | opacity: 0; 147 | transform: translateX(100%); 148 | } 149 | 150 | .example-enter.example-enter-active { 151 | opacity: 1; 152 | transform: translateX(0); 153 | transition: all 0.5s ease-in; 154 | } 155 | 156 | .example-exit { 157 | opacity: 1; 158 | transform: translateX(0); 159 | } 160 | 161 | .example-exit.example-exit-active { 162 | opacity: 0; 163 | transform: translateX(-100%); 164 | transition: all 0.3s ease-out; 165 | } 166 | 167 | .example-appear { 168 | opacity: 0.01; 169 | } 170 | 171 | .example-appear.example-appear-active { 172 | opacity: 1; 173 | transition: opacity 0.5s ease-in; 174 | } 175 | 176 | $headerBar: header-bar; 177 | .#{$headerBar} { 178 | .svg-goback { 179 | padding: 0.3rem; 180 | @include plwh(2rem); 181 | } 182 | .#{$headerBar}-title { 183 | padding: 0.3rem; 184 | } 185 | .#{$headerBar}-antimg { 186 | margin-left: -2rem; 187 | } 188 | } 189 | 190 | .article-operat { 191 | .svg { 192 | @include plwh(1rem); 193 | } 194 | } 195 | 196 | .markdown-body { 197 | font-size: 14px; 198 | word-break: break-all; 199 | overflow: hidden; 200 | h1, 201 | h2 { 202 | margin: 0; 203 | } 204 | h1 { 205 | font-size: 1.8em; 206 | } 207 | img { 208 | max-width: 100%; 209 | } 210 | } 211 | 212 | // 评论 213 | .svg-comment, 214 | .svg-thumbs, 215 | [data-svg-wh] { 216 | @include plwh(1rem); 217 | } 218 | 219 | .comment-title { 220 | padding: 0.4rem 0; 221 | } 222 | 223 | .article-comment-list { 224 | li { 225 | padding: $pd; 226 | border-bottom: 1px dashed $ft-qgrey; 227 | } 228 | .user-thumbs { 229 | margin-right: 1rem; 230 | } 231 | .user-comment-cnt { 232 | overflow: hidden; 233 | } 234 | } 235 | 236 | .fixed-bottom-wrap { 237 | height: 2.5rem; 238 | box-shadow: 0 0 10px rgba($ft-grey, 0.05); 239 | } 240 | 241 | .room-speak { 242 | button, 243 | input { 244 | border: none; 245 | } 246 | .ipt { 247 | height: 2.5rem; 248 | padding-left: 1rem; 249 | } 250 | .ipt-txt-wrap { 251 | &:after, 252 | &:before { 253 | content: ''; 254 | position: absolute; 255 | top: 0; 256 | bottom: 0; 257 | margin: auto; 258 | width: 1px; 259 | height: 1.5rem; 260 | @extend %bg-grey; 261 | } 262 | &:after { 263 | right: 0; 264 | } 265 | &:before { 266 | left: 0.4rem; 267 | } 268 | } 269 | .btn-speak-submit { 270 | // width: 5.3rem; 271 | height: 2.5rem; 272 | color: #00f0ff; 273 | } 274 | .tm-switch-wrap { 275 | width: 22%; 276 | } 277 | .speak-submit-wrap { 278 | width: 15%; 279 | } 280 | } 281 | 282 | //未登录 283 | .not-login-wrap { 284 | a { 285 | margin-left: 0.4rem; 286 | padding: 0.2rem 0.4rem; 287 | border: 1px solid $ft-color; 288 | font-size: 0.8rem; 289 | border-radius: 0.3rem; 290 | } 291 | } 292 | 293 | .not-login-photo { 294 | width: 30%; 295 | margin: 1rem 0; 296 | } 297 | 298 | .user-login-form { 299 | width: 80%; 300 | margin: 60% auto 0; 301 | input { 302 | width: 100%; 303 | height: 2rem; 304 | } 305 | .ipt-row { 306 | margin-top: 10%; 307 | } 308 | } 309 | 310 | .plw { 311 | width: 80%; 312 | margin: 0 auto; 313 | } 314 | 315 | .public-button { 316 | height: 2rem; 317 | } 318 | 319 | //发表 320 | .issue-content-wrap { 321 | input[type='text'], 322 | textarea { 323 | border-bottom: 1px solid $ft-sgrey; 324 | } 325 | textarea { 326 | height: 8rem; 327 | } 328 | @at-root.radio-wrap >span { 329 | position: relative; 330 | @include plwh(1rem); 331 | border: 1px solid $ft-sgrey; 332 | border-radius: 50%; 333 | } 334 | input[type='radio'] { 335 | @extend %tblr; 336 | opacity: 0; 337 | } 338 | .select-radio-active > span { 339 | &:before { 340 | content: ''; 341 | @extend %tblr; 342 | @include plwh(0.6rem); 343 | border-radius: 50%; 344 | background: $ft-color; 345 | } 346 | } 347 | .theme-issue-submit { 348 | margin-top: 0.8rem; 349 | } 350 | } 351 | 352 | //用户中心 353 | .user-center-info { 354 | background-image: linear-gradient(180deg, #7b57bb, $ft-color); 355 | .svg { 356 | @include plwh(1rem); 357 | margin-right: 0.2rem; 358 | } 359 | .user-info-row { 360 | > p { 361 | margin-top: 0.2rem; 362 | } 363 | } 364 | } 365 | 366 | //user tab 367 | .news-content { 368 | .ant-tabs-tab { 369 | width: 50%; 370 | } 371 | } 372 | 373 | .user-center { 374 | .ant-tabs-tab { 375 | width: 33.33%; 376 | } 377 | } 378 | 379 | .clear-unread-msg { 380 | padding: 0.5rem 0.6rem; 381 | } 382 | 383 | .ant-tabs-nav { 384 | width: 100%; 385 | background-color: #fff; 386 | div.ant-tabs-tab { 387 | padding: 0.6rem 0; 388 | text-align: center; 389 | margin-right: 0; 390 | } 391 | .ant-tabs-tab-active, 392 | .ant-tabs-tab:hover { 393 | color: $ft-color; 394 | } 395 | @at-root div.ant-tabs-bar { 396 | margin-bottom: 0; 397 | } 398 | .ant-tabs-ink-bar { 399 | background-color: $ft-color; 400 | } 401 | } 402 | 403 | .title-cnt { 404 | width: 12rem; 405 | } 406 | 407 | .form-msg { 408 | position: fixed; 409 | z-index: 5; 410 | left: 0; 411 | right: 0; 412 | top: 50%; 413 | margin: auto; 414 | padding: 0.6rem 0; 415 | max-width: 10rem; 416 | width: 50%; 417 | background-color: rgba($ft-black, 0.3); 418 | @extend %ft-white; 419 | font-size: 0.8rem; 420 | @include border-radius(0.2rem); 421 | @include translateY(-50%); 422 | } 423 | 424 | .confirm-popups { 425 | width: 70%; 426 | margin: 0 auto; 427 | @extend %bg-white; 428 | @include border-radius(0.2rem); 429 | .confirm-title { 430 | padding: 1.5rem 0; 431 | border-bottom: 1px solid $ft-grey; 432 | } 433 | .confirm-btn { 434 | button { 435 | width: 50%; 436 | padding: 0.8rem 0; 437 | border: none; 438 | background-color: transparent; 439 | } 440 | .confirm-btn-no { 441 | border-right: 1px solid $ft-grey; 442 | } 443 | } 444 | } 445 | 446 | .confirm-report-cnt { 447 | width: 90%; 448 | height: 4rem; 449 | padding: $pd; // border-right: 1px solid $ft-grey; 450 | @include border-radius(0.2rem); 451 | } 452 | -------------------------------------------------------------------------------- /src/assets/styles/css/global.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /*========== 主体颜色 ==========*/ 3 | /*========== 网页字体 ==========*/ 4 | .st-font { 5 | font-family: "simsun", Helvetica Neue, Helvetica, STHeiTi, Arial, sans-serif; } 6 | 7 | /* ========== 字体颜色 ========== */ 8 | [ft-color], .ft-color { 9 | color: #639; } 10 | 11 | .ys-white, [ft-white], 12 | .ft-white, .form-msg { 13 | color: #fff; } 14 | 15 | html, a { 16 | color: #000; } 17 | 18 | .ft-grey { 19 | color: #909ba5; } 20 | 21 | .ft-qgrey { 22 | color: #f3f3f3; } 23 | 24 | .ft-sgrey { 25 | color: #b6b6b6; } 26 | 27 | .ft-orange { 28 | color: #fcd667; } 29 | 30 | /* ========== 字体大小 ========== */ 31 | /* ========== 文本位置 ========== */ 32 | .tc { 33 | text-align: center; } 34 | 35 | .tr { 36 | text-align: right; } 37 | 38 | .tl { 39 | text-align: left; } 40 | 41 | /* ========== 背景颜色 ========== */ 42 | .bg-black { 43 | background: #000; } 44 | 45 | .bg-qblack { 46 | background: #333; } 47 | 48 | .bg-grey, .room-speak .ipt-txt-wrap:after, .room-speak .ipt-txt-wrap:before { 49 | background: #909ba5; } 50 | 51 | .bg-sgrey { 52 | background: #b6b6b6; } 53 | 54 | html, body, .bg-qgrey { 55 | background: #f3f3f3; } 56 | 57 | .bg-white, .confirm-popups { 58 | background-color: #fff; } 59 | 60 | [bg-color], .bg-color { 61 | background-color: #639; } 62 | 63 | .bg-orange { 64 | background-color: #fcd667; } 65 | 66 | [show], .img-tc { 67 | display: block; } 68 | 69 | [hide], 70 | .hide { 71 | display: none; } 72 | 73 | .tblr, .issue-content-wrap input[type='radio'], .issue-content-wrap .select-radio-active > span:before { 74 | position: absolute; 75 | top: 0; 76 | bottom: 0; 77 | left: 0; 78 | right: 0; 79 | margin: auto; } 80 | 81 | /* ========== clear ========== */ 82 | /* ========== opacity ========== */ 83 | html { 84 | -webkit-text-size-adjust: 100%; 85 | -ms-text-size-adjust: 100%; 86 | /* 禁用iPhone中Safari的字号自动调整 */ 87 | text-rendering: optimizelegibility; 88 | max-width: 768px; 89 | margin: auto; 90 | touch-action: none; } 91 | 92 | body { 93 | /*解决上下拉动滚动条时卡顿、慢*/ 94 | -webkit-overflow-scrolling: touch; 95 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 96 | overflow-scrolling: touch; } 97 | 98 | *, 99 | *:after, 100 | *:before { 101 | box-sizing: border-box; } 102 | 103 | article, 104 | aside, 105 | blockquote, 106 | body, 107 | button, 108 | code, 109 | dd, 110 | details, 111 | dl, 112 | dt, 113 | fieldset, 114 | figcaption, 115 | figure, 116 | footer, 117 | form, 118 | h1, 119 | h2, 120 | h3, 121 | h4, 122 | h5, 123 | h6, 124 | header, 125 | hr, 126 | input, 127 | legend, 128 | li, 129 | menu, 130 | nav, 131 | ol, 132 | p, 133 | pre, 134 | section, 135 | td, 136 | textarea, 137 | th, 138 | ul { 139 | margin: 0; 140 | padding: 0; } 141 | 142 | article, 143 | aside, 144 | details, 145 | figcaption, 146 | figure, 147 | footer, 148 | header, 149 | menu, 150 | nav, 151 | section { 152 | display: block; } 153 | 154 | audio, 155 | canvas, 156 | video { 157 | display: inline-block; } 158 | 159 | a, 160 | body, 161 | button, 162 | html, 163 | input, 164 | select, 165 | textarea { 166 | font: 0.7rem/0.8rem 'Helvetica Neue', Helvetica, STHeiTi, Arial, sans-serif; 167 | outline: none; } 168 | 169 | table { 170 | border-collapse: collapse; 171 | border-spacing: 0; } 172 | 173 | fieldset, 174 | iframe, 175 | img { 176 | border: 0; } 177 | 178 | abbr, 179 | acronym { 180 | border-bottom: 1px dotted; 181 | font-variant: normal; } 182 | 183 | abbr { 184 | cursor: help; } 185 | 186 | address, 187 | caption, 188 | cite, 189 | code, 190 | dfn, 191 | em, 192 | th, 193 | var { 194 | font-style: normal; } 195 | 196 | ol, 197 | ul { 198 | list-style: none; } 199 | 200 | caption, 201 | th { 202 | text-align: left; } 203 | 204 | img { 205 | vertical-align: middle; 206 | max-width: 100%; 207 | height: auto; 208 | width: auto\9; 209 | /* ie8 */ 210 | -ms-interpolation-mode: bicubic; 211 | /*为了照顾ie图片缩放失真*/ } 212 | 213 | em { 214 | font-style: normal; } 215 | 216 | a { 217 | border: none; 218 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 219 | -webkit-tap-highlight-color: transparent; 220 | outline: none; 221 | text-decoration: none; } 222 | 223 | a:focus { 224 | outline: none; } 225 | 226 | a.hidefocus { 227 | outline: none; } 228 | 229 | a:hover { 230 | text-decoration: none; } 231 | 232 | mark { 233 | background: #fffdd1; 234 | border-bottom: 1px solid #ffedce; 235 | padding: 2px; 236 | margin: 0 5px; } 237 | 238 | code, 239 | pre, 240 | pre tt { 241 | font-family: Courier, 'Courier New', monospace; } 242 | 243 | figcaption, 244 | small { 245 | font-size: 14px; 246 | color: #888; } 247 | 248 | b, 249 | strong { 250 | font-weight: 700; } 251 | 252 | /*========== headline ==========*/ 253 | h1, 254 | h2, 255 | h3, 256 | h4, 257 | h5, 258 | h6 { 259 | font-weight: normal; } 260 | 261 | h1 { 262 | font-size: 1.2rem; 263 | line-height: 1.6rem; } 264 | 265 | h2 { 266 | font-size: 0.85rem; 267 | line-height: 1.4rem; } 268 | 269 | h3 { 270 | font-size: 0.75rem; 271 | line-height: 1.2rem; } 272 | 273 | h4 { 274 | font-size: 0.6rem; 275 | line-height: 0.8rem; } 276 | 277 | h5 { 278 | font-size: 0.4rem; 279 | line-height: 0.6rem; } 280 | 281 | h6 { 282 | font-size: 0.2rem; 283 | line-height: 0.4rem; } 284 | 285 | /*========== clearfix ==========*/ 286 | .clearfix { 287 | zoom: 1; 288 | display: inline-table; } 289 | 290 | /* Hides from IE-mac \*/ 291 | [clearfix]:after, 292 | [clearfix]:before { 293 | content: ''; 294 | display: table; } 295 | 296 | [clearfix]:after { 297 | clear: both; } 298 | 299 | * html [clearfix] { 300 | height: 1%; } 301 | 302 | /* End hide from IE-mac */ 303 | * + html [clearfix] { 304 | min-height: 1%; } 305 | 306 | /* IE7 */ 307 | .clearFd { 308 | clear: both; } 309 | 310 | /*========== form ==========*/ 311 | input[type='checkbox'] { 312 | vertical-align: middle; } 313 | input[type='checkbox']:checked { 314 | appearance: none; } 315 | 316 | button, 317 | input[type='button'], 318 | input[type='submit'], 319 | input[type='checkbox'], 320 | input[type='reset'] { 321 | cursor: pointer; } 322 | 323 | input[type='text']:focus, 324 | input[type='password']:focus, 325 | textarea:focus, 326 | input[type='email']:focus, 327 | input[type='number']:focus, 328 | input[type='search']:focus, 329 | button:focus, 330 | input[type='reset']:focus, 331 | input[type='submit']:focus, 332 | input[type='button']:focus { 333 | outline: none; } 334 | 335 | button::-moz-focus-inner, 336 | input::-moz-focus-inner { 337 | padding: 0; 338 | border: 0; } 339 | 340 | textarea { 341 | resize: none; 342 | overflow: auto; 343 | -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 344 | -moz-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 345 | box-shadow: 0 0 0 rgba(0, 0, 0, 0); 346 | -webkit-appearance: none; } 347 | 348 | /* 去除iPhone中默认的input样式 */ 349 | input { 350 | resize: none; } 351 | 352 | button { 353 | border: none; } 354 | 355 | button, 356 | input { 357 | outline: none; 358 | appearance: none; 359 | -webkit-appearance: none; 360 | border-radius: 0; 361 | -webkit-tap-highlight-color: transparent; 362 | -webkit-touch-callout: none; 363 | /*长时间按住页面出现闪退*/ } 364 | 365 | /*========== Delete button under Firefox default style ==========*/ 366 | input[type='reset']::-moz-focus-inner, 367 | input[type='button']::-moz-focus-inner, 368 | input[type='submit']::-moz-focus-inner, 369 | input[type='file'] > input[type='button']::-moz-focus-inner { 370 | border: none; 371 | padding: 0; } 372 | 373 | .container { 374 | min-height: 100%; 375 | padding: 1.9rem 0 2.7rem; } 376 | 377 | .container-top { 378 | padding-top: 2rem; } 379 | 380 | .container-bottom { 381 | padding-bottom: 2.7rem; } 382 | 383 | [data-layout-flex='0'] { 384 | -webkit-flex: none; 385 | flex: none; } 386 | 387 | [layout], 388 | [data-layout='layout'] { 389 | display: -webkit-flex; 390 | display: flex; } 391 | 392 | [data-layout-wrap], 393 | [flex] { 394 | -webkit-flex: 1; 395 | flex: 1; } 396 | 397 | [data-layout-wrap], 398 | [layout-wrap] { 399 | -webkit-flex-wrap: wrap; 400 | flex-wrap: wrap; } 401 | 402 | [data-layout='row'], 403 | [layout='row'] { 404 | -webkit-flex-direction: row; 405 | flex-direction: row; } 406 | 407 | [data-layout='column'], 408 | [layout='column'] { 409 | -webkit-flex-direction: column; 410 | flex-direction: column; } 411 | 412 | [data-layout-align='start start'], 413 | [data-layout-align='start center'], 414 | [data-layout-align='start end'] [layout-align='start start'], 415 | [layout-align='start center'], 416 | [layout-align='start end'] { 417 | -webkit-justify-content: flex-start; 418 | justify-content: flex-start; } 419 | 420 | [data-layout-align='center start'], 421 | [data-layout-align='center center'], 422 | [data-layout-align='center end'], 423 | [layout-align='center start'], 424 | [layout-align='center center'], 425 | [layout-align='center end'] { 426 | -webkit-justify-content: center; 427 | justify-content: center; } 428 | 429 | [data-layout-align='end start'], 430 | [data-layout-align='end center'], 431 | [data-layout-align='end end'] [layout-align='end start'], 432 | [layout-align='end center'], 433 | [layout-align='end end'] { 434 | -webkit-justify-content: flex-end; 435 | justify-content: flex-end; } 436 | 437 | [data-layout-align='space-between start'], 438 | [data-layout-align='space-between center'], 439 | [data-layout-align='space-between end'], 440 | [layout-align='space-between start'], 441 | [layout-align='space-between center'], 442 | [layout-align='space-between end'] { 443 | -webkit-justify-content: space-between; 444 | justify-content: space-between; } 445 | 446 | [data-layout-align='space-arround start'], 447 | [data-layout-align='space-arround center'], 448 | [data-layout-align='space-arround end'] [layout-align='space-arround start'], 449 | [layout-align='space-arround center'], 450 | [layout-align='space-arround end'] { 451 | -webkit-justify-content: space-arround; 452 | justify-content: space-arround; } 453 | 454 | [data-layout-align='start start'], 455 | [data-layout-align='center start'], 456 | [data-layout-align='end start'], 457 | [data-layout-align='space-between start'], 458 | [data-layout-align='space-arround start'] [layout-align='start start'], 459 | [layout-align='center start'], 460 | [layout-align='end start'], 461 | [layout-align='space-between start'], 462 | [layout-align='space-arround start'] { 463 | -webkit-align-items: flex-start; 464 | align-items: flex-start; } 465 | 466 | [data-layout-align='start center'], 467 | [data-layout-align='center center'], 468 | [data-layout-align='end center'], 469 | [data-layout-align='space-between center'], 470 | [data-layout-align='space-arround center'] [layout-align='start center'], 471 | [layout-align='center center'], 472 | [layout-align='end center'], 473 | [layout-align='space-between center'], 474 | [layout-align='space-arround center'] { 475 | -webkit-align-items: center; 476 | align-items: center; } 477 | 478 | [data-layout-align='start end'], 479 | [data-layout-align='center end'], 480 | [data-layout-align='end end'], 481 | [data-layout-align='space-between end'], 482 | [data-layout-align='space-arround end'] [layout-align='start end'], 483 | [layout-align='center end'], 484 | [layout-align='end end'], 485 | [layout-align='space-between end'], 486 | [layout-align='space-arround end'] { 487 | -webkit-align-items: flex-end; 488 | align-items: flex-end; } 489 | 490 | /* ========== public ========== */ 491 | .fl { 492 | display: inline; 493 | float: left; } 494 | 495 | .fr { 496 | display: inline; 497 | float: right; } 498 | 499 | .pr { 500 | position: relative; } 501 | 502 | .pa { 503 | position: absolute; } 504 | 505 | .pf { 506 | position: fixed; } 507 | 508 | .pl { 509 | padding-left: 0.4rem; } 510 | 511 | .t0 { 512 | top: 0; } 513 | 514 | .b0 { 515 | bottom: 0; } 516 | 517 | .l0 { 518 | left: 0; } 519 | 520 | .r0 { 521 | right: 0; } 522 | 523 | .tl0 { 524 | top: 0; 525 | left: 0; } 526 | 527 | .bl0 { 528 | bottom: 0; 529 | left: 0; } 530 | 531 | .lr0 { 532 | right: 0; 533 | left: 0; 534 | margin: auto; } 535 | 536 | .tb0 { 537 | top: 0; 538 | bottom: 0; } 539 | 540 | .z1 { 541 | z-index: 1; } 542 | 543 | .z3 { 544 | z-index: 3; } 545 | 546 | .z5 { 547 | z-index: 5; } 548 | 549 | .wh100 { 550 | width: 100%; 551 | height: 100%; } 552 | 553 | .ilbk, 554 | .icons, 555 | .icon, 556 | .svg, 557 | [ilbk] { 558 | display: -moz-inline-stack; 559 | display: inline-block; 560 | vertical-align: middle; 561 | *vertical-align: auto; 562 | zoom: 1; 563 | *display: inline; } 564 | 565 | .mgt { 566 | margin-top: 0.4rem; } 567 | 568 | .rdir-mgt { 569 | margin-top: -0.4rem; } 570 | 571 | .mgb { 572 | margin-bottom: 0.4rem; } 573 | 574 | .mgl { 575 | margin-left: 0.4rem; } 576 | 577 | .mgr { 578 | margin-right: 0.4rem; } 579 | 580 | .mgtb { 581 | margin: 0.4rem 0; } 582 | 583 | .mglr { 584 | margin: 0 0.4rem; } 585 | 586 | .mgt { 587 | margin-top: 0.4rem; } 588 | 589 | .pd { 590 | padding: 0.4rem; } 591 | 592 | .pdtb { 593 | padding: 0.4rem 0; } 594 | 595 | .pdtr { 596 | padding-top: 0.4rem; 597 | padding-right: 0.4rem; } 598 | 599 | .pdlr { 600 | padding: 0 0.4rem; } 601 | 602 | .pdtl { 603 | padding-top: 0.4rem; 604 | padding-left: 0.4rem; } 605 | 606 | .pdlb { 607 | padding-bottom: 0.4rem; 608 | padding-left: 0.4rem; } 609 | 610 | .pdtbl { 611 | padding: 0.4rem 0 0.4rem 0.4rem; } 612 | 613 | .pdl { 614 | padding-left: 0.4rem; } 615 | 616 | .pdt { 617 | padding-top: 0.4rem; } 618 | 619 | .pdr { 620 | padding-right: 0.4rem; } 621 | 622 | .pdb { 623 | padding-bottom: 0.4rem; } 624 | 625 | .w-half { 626 | width: 50%; } 627 | 628 | 629 | .fz8 { 630 | font-size: 0.8rem; } 631 | 632 | [ellipsis], 633 | [data-ellipsis] { 634 | white-space: nowrap; 635 | overflow: hidden; 636 | -ms-text-overflow: ellipsis; 637 | -o-text-overflow: ellipsis; 638 | text-overflow: ellipsis; } 639 | 640 | .ft-purple { 641 | color: #00f0ff; } 642 | 643 | .w100 { 644 | width: 100%; } 645 | 646 | .h100 { 647 | height: 100%; } 648 | 649 | .ft-bold { 650 | font-weight: 900; } 651 | 652 | .ft-normal { 653 | font-weight: normal; } 654 | 655 | [ft-hidden] { 656 | overflow: hidden; 657 | text-overflow: ellipsis; 658 | display: -webkit-box; 659 | -webkit-box-orient: vertical; } 660 | 661 | .vam { 662 | vertical-align: middle; } 663 | 664 | .img-tc { 665 | max-width: 100%; 666 | margin-left: auto; 667 | margin-right: auto; } 668 | 669 | .dp-flex { 670 | display: -webkit-flex; 671 | display: flex; } 672 | 673 | .bg-transparent { 674 | background-color: rgba(0, 0, 0, 0.5); } 675 | 676 | .bg-color-none { 677 | background-color: transparent; } 678 | 679 | .bdr-half { 680 | -webkit-border-radius: 50%; 681 | -moz-border-radius: 50%; 682 | -ms-border-radius: 50%; 683 | -o-border-radius: 50%; 684 | border-radius: 50%; } 685 | 686 | /* ========== flex ========== */ 687 | .numFt { 688 | font-family: 'Georgia'; } 689 | 690 | .bd-radius { 691 | border-radius: .3rem; } 692 | 693 | .bd-none { 694 | border: none; } 695 | 696 | .small-font { 697 | font-size: .6rem; } 698 | 699 | .tab-wrap { 700 | position: fixed; 701 | top: 0; 702 | left: 0; 703 | display: table; 704 | width: 100%; 705 | height: 100%; } 706 | 707 | .tab-wrap-cell { 708 | display: table-cell; 709 | vertical-align: middle; } 710 | 711 | .mask-layer, .mask-page { 712 | position: fixed; 713 | z-index: 11; 714 | top: 0; 715 | left: 0; 716 | width: 100%; 717 | height: 100%; 718 | background-color: #000; 719 | background-color: rgba(0, 0, 0, 0.5); } 720 | 721 | .mask-page { 722 | z-index: 1; } 723 | 724 | #root { 725 | height: 100%; } 726 | 727 | a:hover { 728 | color: #639; } 729 | 730 | .header .head-logo { 731 | padding: 0.6rem 0; 732 | font-size: 0.8rem; } 733 | 734 | .tab-nav-list li, 735 | .footer-menu li { 736 | text-align: center; } 737 | 738 | .tab-nav-list a, 739 | .footer-menu a { 740 | display: block; 741 | padding: 0.4rem 0; } 742 | 743 | .tab-nav-list li { 744 | width: 20%; } 745 | 746 | .nav-top-fixed { 747 | box-shadow: 0 0 5px rgba(144, 155, 165, 0.1); } 748 | 749 | .footer-menu { 750 | width: 100%; 751 | background-color: #fff; 752 | box-shadow: 0 0 10px rgba(144, 155, 165, 0.05); } 753 | .footer-menu li { 754 | width: 25%; } 755 | .footer-menu a { 756 | padding: 0.2rem 0; } 757 | .footer-menu .svg { 758 | width: 1.5rem; 759 | height: 1.5rem; } 760 | .footer-menu .svg-nav-active { 761 | display: none; } 762 | .footer-menu .nav-selected .svg-nav-active { 763 | display: inline-block; } 764 | .footer-menu .nav-selected .svg-nav-default { 765 | display: none; } 766 | .footer-menu .nav-selected p { 767 | color: #639; } 768 | .footer-menu .nav-selected .news-number { 769 | background-color: #639; } 770 | .footer-menu .news-number { 771 | width: 1rem; 772 | height: 1rem; 773 | right: 1.2rem; 774 | top: 0; } 775 | 776 | .photo { 777 | width: 10rem; 778 | height: 10rem; } 779 | 780 | .scroll-wrap { 781 | padding-top: 1.7rem; } 782 | 783 | .iscroll-wrapper.loadmore-wrap { 784 | top: 3.6rem; 785 | bottom: 2.6rem; } 786 | 787 | .loadmore-content { 788 | padding: 1.6rem 0 2.7rem; } 789 | 790 | .article-item { 791 | padding: 0.4rem; 792 | background-color: #fff; } 793 | .author-avatar { 794 | width: 4rem; 795 | height: 4rem; } 796 | .small-avatar-wrap { 797 | width: 2.5rem; 798 | height: 2.5rem; } 799 | .small-author-avatar { 800 | width: 2rem; 801 | height: 2rem; } 802 | .article-item .title-type { 803 | padding: 0 0.2rem; } 804 | .article-item .svg { 805 | width: 1rem; 806 | height: 1rem; } 807 | 808 | .ant-back-top { 809 | width: 2.5rem; 810 | height: 2.5rem; 811 | position: fixed; 812 | bottom: 4rem; 813 | right: 1rem; 814 | border-radius: 50%; } 815 | .ant-back-top .svg-gotop { 816 | width: 1.8rem; 817 | height: 1.8rem; } 818 | 819 | .view-main { 820 | transition: 0.5s; } 821 | 822 | .hideLeft { 823 | opacity: 0.75; 824 | transform: translate3d(-100%, 0, 0); 825 | transition: 0.5s; } 826 | 827 | .hideRight { 828 | opacity: 0.75; 829 | transform: translate3d(100%, 0, 0); 830 | transition: 0.5s; } 831 | 832 | .example-enter { 833 | opacity: 0; 834 | transform: translateX(100%); } 835 | 836 | .example-enter.example-enter-active { 837 | opacity: 1; 838 | transform: translateX(0); 839 | transition: all 0.5s ease-in; } 840 | 841 | .example-exit { 842 | opacity: 1; 843 | transform: translateX(0); } 844 | 845 | .example-exit.example-exit-active { 846 | opacity: 0; 847 | transform: translateX(-100%); 848 | transition: all 0.3s ease-out; } 849 | 850 | .example-appear { 851 | opacity: 0.01; } 852 | 853 | .example-appear.example-appear-active { 854 | opacity: 1; 855 | transition: opacity 0.5s ease-in; } 856 | 857 | .header-bar .svg-goback { 858 | padding: 0.3rem; 859 | width: 2rem; 860 | height: 2rem; } 861 | 862 | .header-bar .header-bar-title { 863 | padding: 0.3rem; } 864 | 865 | .header-bar .header-bar-antimg { 866 | margin-left: -2rem; } 867 | 868 | .article-operat .svg { 869 | width: 1rem; 870 | height: 1rem; } 871 | 872 | .markdown-body { 873 | font-size: 14px; 874 | word-break: break-all; 875 | overflow: hidden; } 876 | .markdown-body h1, 877 | .markdown-body h2 { 878 | margin: 0; } 879 | .markdown-body h1 { 880 | font-size: 1.8em; } 881 | .markdown-body img { 882 | max-width: 100%; } 883 | 884 | .svg-comment, 885 | .svg-thumbs, 886 | [data-svg-wh] { 887 | width: 1rem; 888 | height: 1rem; } 889 | 890 | .comment-title { 891 | padding: 0.4rem 0; } 892 | 893 | .article-comment-list li { 894 | padding: 0.4rem; 895 | border-bottom: 1px dashed #f3f3f3; } 896 | 897 | .article-comment-list .user-thumbs { 898 | margin-right: 1rem; } 899 | 900 | .article-comment-list .user-comment-cnt { 901 | overflow: hidden; } 902 | 903 | .fixed-bottom-wrap { 904 | height: 2.5rem; 905 | box-shadow: 0 0 10px rgba(144, 155, 165, 0.05); } 906 | 907 | .room-speak button, 908 | .room-speak input { 909 | border: none; } 910 | 911 | .room-speak .ipt { 912 | height: 2.5rem; 913 | padding-left: 1rem; } 914 | 915 | .room-speak .ipt-txt-wrap:after, .room-speak .ipt-txt-wrap:before { 916 | content: ''; 917 | position: absolute; 918 | top: 0; 919 | bottom: 0; 920 | margin: auto; 921 | width: 1px; 922 | height: 1.5rem; } 923 | 924 | .room-speak .ipt-txt-wrap:after { 925 | right: 0; } 926 | 927 | .room-speak .ipt-txt-wrap:before { 928 | left: 0.4rem; } 929 | 930 | .room-speak .btn-speak-submit { 931 | height: 2.5rem; 932 | color: #00f0ff; } 933 | 934 | .room-speak .tm-switch-wrap { 935 | width: 22%; } 936 | 937 | .room-speak .speak-submit-wrap { 938 | width: 15%; } 939 | 940 | .not-login-wrap a { 941 | margin-left: 0.4rem; 942 | padding: 0.2rem 0.4rem; 943 | border: 1px solid #639; 944 | font-size: 0.8rem; 945 | border-radius: 0.3rem; } 946 | 947 | .not-login-photo { 948 | width: 30%; 949 | margin: 1rem 0; } 950 | 951 | .user-login-form { 952 | width: 80%; 953 | margin: 60% auto 0; } 954 | .user-login-form input { 955 | width: 100%; 956 | height: 2rem; } 957 | .user-login-form .ipt-row { 958 | margin-top: 10%; } 959 | 960 | .plw { 961 | width: 80%; 962 | margin: 0 auto; } 963 | 964 | .public-button { 965 | height: 2rem; } 966 | 967 | .issue-content-wrap input[type='text'], 968 | .issue-content-wrap textarea { 969 | border-bottom: 1px solid #b6b6b6; } 970 | 971 | .issue-content-wrap textarea { 972 | height: 8rem; } 973 | 974 | .radio-wrap > span { 975 | position: relative; 976 | width: 1rem; 977 | height: 1rem; 978 | border: 1px solid #b6b6b6; 979 | border-radius: 50%; } 980 | 981 | .issue-content-wrap input[type='radio'] { 982 | opacity: 0; } 983 | 984 | .issue-content-wrap .select-radio-active > span:before { 985 | content: ''; 986 | width: 0.6rem; 987 | height: 0.6rem; 988 | border-radius: 50%; 989 | background: #639; } 990 | 991 | .issue-content-wrap .theme-issue-submit { 992 | margin-top: 0.8rem; } 993 | 994 | .user-center-info { 995 | background-image: linear-gradient(180deg, #7b57bb, #639); } 996 | .user-center-info .svg { 997 | width: 1rem; 998 | height: 1rem; 999 | margin-right: 0.2rem; } 1000 | .user-center-info .user-info-row > p { 1001 | margin-top: 0.2rem; } 1002 | 1003 | .news-content .ant-tabs-tab { 1004 | width: 50%; } 1005 | 1006 | .user-center .ant-tabs-tab { 1007 | width: 33.33%; } 1008 | 1009 | .clear-unread-msg { 1010 | padding: 0.5rem 0.6rem; } 1011 | 1012 | .ant-tabs-nav { 1013 | width: 100%; 1014 | background-color: #fff; } 1015 | .ant-tabs-nav div.ant-tabs-tab { 1016 | padding: 0.6rem 0; 1017 | text-align: center; 1018 | margin-right: 0; } 1019 | .ant-tabs-nav .ant-tabs-tab-active, 1020 | .ant-tabs-nav .ant-tabs-tab:hover { 1021 | color: #639; } 1022 | div.ant-tabs-bar { 1023 | margin-bottom: 0; } 1024 | .ant-tabs-nav .ant-tabs-ink-bar { 1025 | background-color: #639; } 1026 | 1027 | .title-cnt { 1028 | width: 12rem; } 1029 | 1030 | .form-msg { 1031 | position: fixed; 1032 | z-index: 5; 1033 | left: 0; 1034 | right: 0; 1035 | top: 50%; 1036 | margin: auto; 1037 | padding: 0.6rem 0; 1038 | max-width: 10rem; 1039 | width: 50%; 1040 | background-color: rgba(0, 0, 0, 0.3); 1041 | font-size: 0.8rem; 1042 | -webkit-border-radius: 0.2rem; 1043 | -moz-border-radius: 0.2rem; 1044 | -ms-border-radius: 0.2rem; 1045 | -o-border-radius: 0.2rem; 1046 | border-radius: 0.2rem; 1047 | -webkit-transform: translateY(-50%); 1048 | -moz-transform: translateY(-50%); 1049 | -ms-transform: translateY(-50%); 1050 | -o-transform: translateY(-50%); 1051 | transform: translateY(-50%); } 1052 | 1053 | .confirm-popups { 1054 | width: 70%; 1055 | margin: 0 auto; 1056 | -webkit-border-radius: 0.2rem; 1057 | -moz-border-radius: 0.2rem; 1058 | -ms-border-radius: 0.2rem; 1059 | -o-border-radius: 0.2rem; 1060 | border-radius: 0.2rem; } 1061 | .confirm-popups .confirm-title { 1062 | padding: 1.5rem 0; 1063 | border-bottom: 1px solid #909ba5; } 1064 | .confirm-popups .confirm-btn button { 1065 | width: 50%; 1066 | padding: 0.8rem 0; 1067 | border: none; 1068 | background-color: transparent; } 1069 | .confirm-popups .confirm-btn .confirm-btn-no { 1070 | border-right: 1px solid #909ba5; } 1071 | 1072 | .confirm-report-cnt { 1073 | width: 90%; 1074 | height: 4rem; 1075 | padding: 0.4rem; 1076 | -webkit-border-radius: 0.2rem; 1077 | -moz-border-radius: 0.2rem; 1078 | -ms-border-radius: 0.2rem; 1079 | -o-border-radius: 0.2rem; 1080 | border-radius: 0.2rem; } 1081 | -------------------------------------------------------------------------------- /src/component/ArticleItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import LazyImage from '@component/common/LazyImage' 4 | 5 | import { formatDate } from '@script/utils' 6 | 7 | import '@fonts/svg/write.svg' 8 | import '@fonts/svg/page-views.svg' 9 | 10 | const TitleType = props => ( 11 | {props.type} 12 | ) 13 | TitleType.defaultProps = { 14 | color: 'bg-sgrey' 15 | } 16 | 17 | const ArticleItem = props => { 18 | const item = props.info 19 | return ( 20 |
23 | {props.isLinks ? ( 24 | 33 | 34 | 35 | ) : ( 36 | 37 | )} 38 |
39 | ) 40 | } 41 | 42 | ArticleItem.defaultProps = { 43 | isLinks: true 44 | } 45 | 46 | const TopicItem = props => { 47 | const item = props.info 48 | return ( 49 |
50 | {props.isLazyImg ? ( 51 | 57 | ) : ( 58 | 64 | )} 65 |
66 |

67 | {/* */} 77 | {(() => { 78 | if (item.top) { 79 | return 80 | } 81 | if (item.good) { 82 | return 83 | } 84 | switch (item.tab) { 85 | case 'share': 86 | return 87 | case 'good': 88 | return 89 | case 'ask': 90 | return 91 | case 'job': 92 | return 93 | case 'dev': 94 | return 95 | default: 96 | return '' 97 | } 98 | })()} 99 | {item.title} 100 |

101 |
106 | 107 | 108 | 109 | 110 | {item.author.loginname} 111 | 112 | {formatDate(item.create_at)} 113 |
114 |
119 | 120 | 121 | 122 | 123 | {item.reply_count}/{item.visit_count} 124 | 125 | {formatDate(item.last_reply_at)} 126 |
127 |
128 |
129 | ) 130 | } 131 | 132 | TopicItem.defaultProps = { 133 | isLazyImg: true 134 | } 135 | 136 | export default ArticleItem 137 | -------------------------------------------------------------------------------- /src/component/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | export default () => ( 3 |
8 | NodeJS论坛 9 |
10 | ) 11 | -------------------------------------------------------------------------------- /src/component/Menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NavLink, withRouter } from 'react-router-dom' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | 6 | import ActionNews from '@store/actions/ActionNews' 7 | 8 | import '@fonts/svg/nall.svg' 9 | import '@fonts/svg/nask.svg' 10 | import '@fonts/svg/njob.svg' 11 | import '@fonts/svg/ngood.svg' 12 | import '@fonts/svg/nshare.svg' 13 | import '@fonts/svg/nhome.svg' 14 | import '@fonts/svg/nnews.svg' 15 | import '@fonts/svg/nissue.svg' 16 | import '@fonts/svg/nuser.svg' 17 | 18 | class TabFooNav extends React.Component { 19 | constructor(props) { 20 | super(props) 21 | this.state = { 22 | num: 0 23 | } 24 | this.isLogin = this.$getStorage() 25 | } 26 | /* componentWillReceiveProps(prevProps, prevState) { 27 | const {result} = this.props.ReadNews; 28 | result && result.success && this.setState({num: result.data}); 29 | } */ 30 | componentDidMount() { 31 | this.isLogin && this.props.actions.fetchData(this.isLogin.accesstoken) 32 | } 33 | render() { 34 | const { result } = this.props.ReadNews 35 | const tabmenu = this.props.tabnav.map((item, index) => { 36 | const isNewsTips = this.isLogin && item.type === 'news' 37 | return ( 38 |
  • 39 | { 40 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {isNewsTips && ( 56 | 61 | {result && result.success ? result.data : 0} 62 | 63 | )} 64 |

    {item.name}

    65 |
    66 | } 67 |
  • 68 | ) 69 | }) 70 | return ( 71 | 74 | ) 75 | } 76 | } 77 | 78 | class Menu extends React.Component { 79 | componentWillReceiveProps(nextProps) {} 80 | render() { 81 | const getLogin = this.$getStorage() 82 | // console.log(this.props.rTopicList.topicType) 83 | const meLink = getLogin && getLogin.loginname ? `/user` : '/user/login' 84 | const tabnav = [ 85 | { 86 | name: '首页', 87 | type: 'home', 88 | link: '/', 89 | search: `?tab=${this.props.rTopicList.topicType}` 90 | }, 91 | { 92 | name: '消息', 93 | type: 'news', 94 | link: '/news' 95 | }, 96 | { 97 | name: '发表', 98 | type: 'issue', 99 | link: '/share' 100 | }, 101 | { 102 | name: '我的', 103 | type: 'user', 104 | link: meLink, 105 | search: getLogin ? `?name=${getLogin.loginname}` : '' 106 | } 107 | ] 108 | return ( 109 |
    110 | 111 |
    112 | ) 113 | } 114 | } 115 | 116 | const mapStateToProps = state => { 117 | return { ReadNews: state.ReadNews, rTopicList: state.rTopicList } 118 | } 119 | const actions = { 120 | ...ActionNews 121 | } 122 | const mapDispatchToProps = dispatch => ({ 123 | actions: bindActionCreators(actions, dispatch) 124 | }) 125 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Menu)) 126 | -------------------------------------------------------------------------------- /src/component/SubHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route } from 'react-router-dom' 3 | import '@fonts/svg/goback.svg' 4 | const SubHeader = props => ( 5 | { 7 | const arrPathName = ['/share', '/', '/news', '/user'] 8 | const isDefaultPath = arrPathName.includes(location.pathname) 9 | return ( 10 |
    15 | {(!isDefaultPath || props.isBack) && ( 16 | 17 | 18 | 19 | )} 20 | {props.children} 21 |

    22 | 23 | {props.title} 24 | 25 |

    26 |
    27 | ) 28 | }} 29 | /> 30 | ) 31 | SubHeader.defaultProps = { 32 | title: '默认标题', 33 | isBack: false 34 | } 35 | export default SubHeader 36 | -------------------------------------------------------------------------------- /src/component/common/LazyImage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Image from 'react-lazy-image' 3 | export default props => ( 4 | 9 | ) 10 | -------------------------------------------------------------------------------- /src/component/common/MaskPopups.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | export default class MaskPopups extends React.Component { 3 | constructor(props) { 4 | super(props) 5 | this.state = { 6 | isPopups: false 7 | } 8 | } 9 | deterMine() { 10 | this.props.SendDeterMine() 11 | } 12 | cancelRun() { 13 | this.props.SendCancelRun() 14 | } 15 | render() { 16 | return ( 17 |
    18 |
    19 |
    20 |
    21 |

    {this.props.title}

    22 |
    27 | 33 | 39 |
    40 |
    41 |
    42 |
    43 |
    44 | ) 45 | } 46 | } 47 | 48 | MaskPopups.defaultProps = { 49 | title: '默认弹窗标题', 50 | SendCancelRun: () => {}, 51 | SendDeterMine: () => {} 52 | } 53 | -------------------------------------------------------------------------------- /src/component/common/SharedCompt.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | // import PropTypes from "prop-types"; 4 | 5 | import { setStorage } from '@script/utils' 6 | 7 | export class UserLogin extends React.Component { 8 | // static contextTypes = { router: PropTypes.object } 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | btnval: '登录' 13 | } 14 | this.loginSubmit = this.loginSubmit.bind(this) 15 | } 16 | enterSumit(e) { 17 | Object.is(e.keyCode, 13) && this.loginSubmit() 18 | } 19 | loginSubmit() { 20 | const UserTokenVal = this.refs.userToken.value 21 | if (!UserTokenVal) { 22 | return this.$showMsg('亲,token不能为空!') 23 | } 24 | this.setState({ btnval: '登录中...' }) 25 | this.$request({ 26 | method: 'post', 27 | url: `accesstoken`, 28 | data: { 29 | accesstoken: UserTokenVal 30 | }, 31 | success: result => { 32 | if (result.success) { 33 | this.$showMsg('登录成功!') 34 | result['accesstoken'] = UserTokenVal 35 | setStorage('USER_INFO', result) 36 | this.props.history.push({ 37 | pathname: '/user', 38 | search: `?name=${result.loginname}` 39 | }) 40 | } else { 41 | this.$showMsg('登录失败!') 42 | this.setState({ btnval: '登录' }) 43 | } 44 | }, 45 | error: () => { 46 | this.$showMsg('登录失败!') 47 | this.setState({ btnval: '登录' }) 48 | } 49 | }) 50 | } 51 | render() { 52 | return ( 53 |
    54 |
    55 | 63 |
    64 |
    65 | 72 |
    73 |
    74 | ) 75 | } 76 | } 77 | 78 | export const NotLogin = props => { 79 | return ( 80 |
    81 | {props.isLogo && ( 82 | 87 | )} 88 |

    89 | 还未登录,现在就去 90 | 登录 91 | 92 |

    93 |
    94 | ) 95 | } 96 | 97 | NotLogin.defaultProps = { 98 | isLogo: true 99 | } 100 | 101 | export const DetectLogin = props => { 102 | class LoginStatus extends React.Component { 103 | // constructor(props) { 104 | // super(props) 105 | // } 106 | render() { 107 | if (this.$getStorage()) return 108 | return 109 | } 110 | } 111 | return LoginStatus 112 | } 113 | 114 | export const LoadLoop = () =>
    玩命加载中...
    115 | export const LoadFail = () =>
    加载失败!
    116 | export const NotResult = props =>
    {props.text}
    117 | 118 | NotResult.defaultProps = { 119 | text: '暂无数据!' 120 | } 121 | -------------------------------------------------------------------------------- /src/component/plugin/touch-loadmore/LoadMore.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import Tloader from './react-touch-loader' 4 | import ArticleItem from '@component/ArticleItem' 5 | import aHomeTopic from '@store/actions/ActionTopicList' 6 | import './load-more.less' 7 | class LoadMore extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | canRefreshResolve: 1, 12 | hasMore: 1, 13 | infoList: props.rTopicList.result, 14 | isLoadData: true, 15 | initializing: props.rTopicList.result.length ? 2 : 1, 16 | refreshedAt: Date.now() 17 | } 18 | this.refScroll = null 19 | } 20 | componentWillReceiveProps(nextProps) { 21 | !Object.is(nextProps.type, this.props.type) && 22 | this.setState( 23 | { 24 | // infoList: [], 25 | hasMore: 1 26 | }, 27 | () => { 28 | this.refresh() 29 | } 30 | ) 31 | } 32 | componentDidMount() { 33 | const { page, scrollTop } = this.props.rTopicList 34 | !page && this.loadData() 35 | !this.refScroll && (this.refScroll = this.refs.scrollCnt.refs.panel) 36 | scrollTop && (this.refScroll.scrollTop = scrollTop) 37 | } 38 | refresh(resolve, reject) { 39 | this.setState( 40 | { 41 | hasMore: 1 42 | }, 43 | () => { 44 | this.loadData(resolve, 'reset') 45 | } 46 | ) 47 | } 48 | componentWillUnmount() { 49 | const { dispatch } = this.props 50 | dispatch(aHomeTopic.recordScrollSite(this.refScroll.scrollTop)) //保存滚动条位置 51 | } 52 | 53 | loadData(resolve, reset) { 54 | //加载数据 55 | const { infoList } = this.state 56 | const isReset = reset === 'reset' 57 | const page = isReset ? 1 : Math.ceil(infoList.length / 10) + 1 58 | this.$request({ 59 | url: 'topics', 60 | params: { 61 | page, 62 | tab: this.props.type, 63 | limit: 10 64 | }, 65 | success: result => { 66 | if (this.state.isLoadData) { 67 | const { data } = result 68 | this.setState({ 69 | infoList: isReset ? data : infoList.concat(data), 70 | hasMore: 1, 71 | initializing: 2, 72 | // initialized 73 | loadState: 'loaded' 74 | }) 75 | const { dispatch, rTopicList, type } = this.props 76 | ;(!Object.is(rTopicList.page, page) || isReset) && 77 | dispatch( 78 | aHomeTopic.keepTopic({ 79 | data, 80 | page, 81 | type, 82 | isReset 83 | }) 84 | ) 85 | if (isReset) { 86 | //不同类型 or 刷新重置滚动条 87 | window.scrollTo(0, 0) 88 | this.refScroll.scrollTop = 0 89 | } 90 | // if (Math.ceil(infoList.length / 10) === 2) { 91 | if (data.length < 10) { 92 | this.setState({ hasMore: false }) 93 | } 94 | resolve && resolve() 95 | } 96 | } 97 | }) 98 | } 99 | render() { 100 | const { hasMore, initializing } = this.state 101 | const { infoList } = this.state 102 | return ( 103 |
    104 | 112 |
    113 | {infoList.map((item, index) => ( 114 | 120 | ))} 121 |
    122 |
    123 |
    124 | ) 125 | } 126 | } 127 | 128 | export default connect((state, action) => { 129 | return { 130 | rTopicList: state.rTopicList 131 | } 132 | })(LoadMore) 133 | -------------------------------------------------------------------------------- /src/component/plugin/touch-loadmore/load-more.less: -------------------------------------------------------------------------------- 1 | :not(input):not(textarea):not([contenteditable]) { 2 | -webkit-touch-callout: none; 3 | user-select: none; 4 | } 5 | 6 | .load-more-view { 7 | position: absolute; 8 | top: 10%; 9 | left: 0; 10 | right: 0; 11 | height: 100%; 12 | overflow: hidden; 13 | background-color: #EFEFF4; 14 | display: flex; 15 | flex-direction: column; // header, footer 16 | h1, 17 | h2 { 18 | margin: 0; 19 | background-color: #333; 20 | color: #eee; 21 | font-size: 1em; 22 | line-height: 3; 23 | a { 24 | color: inherit; 25 | margin: 0 1em; 26 | } 27 | } 28 | .main { 29 | flex: 1; 30 | overflow-x: hidden; 31 | overflow-y: scroll; 32 | -webkit-overflow-scrolling: touch; // enhance ios scrolling 33 | ul { 34 | list-style: none; 35 | margin: 0; 36 | padding: 0; 37 | li { 38 | background-color: #fff; 39 | border-bottom: 1px solid #ccc; 40 | p { 41 | margin: 0; 42 | line-height: 3em; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/component/plugin/touch-loadmore/react-touch-loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './touch-loader.less' 3 | 4 | const STATS = { 5 | init: '', 6 | pulling: 'pulling', 7 | enough: 'pulling enough', 8 | refreshing: 'refreshing', 9 | refreshed: 'refreshed', 10 | reset: 'reset', 11 | 12 | loading: 'loading' // loading more 13 | } 14 | 15 | // pull to refresh 16 | // tap bottom to load more 17 | class Tloader extends React.Component { 18 | constructor() { 19 | super() 20 | this.state = { 21 | loaderState: STATS.init, 22 | pullHeight: 0, 23 | progressed: 0 24 | } 25 | this.loadMore = this.loadMore.bind(this) 26 | } 27 | 28 | setInitialTouch(touch) { 29 | this._initialTouch = { 30 | clientY: touch.clientY 31 | } 32 | } 33 | calculateDistance(touch) { 34 | return touch.clientY - this._initialTouch.clientY 35 | } 36 | // 拖拽的缓动公式 - easeOutSine 37 | easing(distance) { 38 | // t: current time, b: begInnIng value, c: change In value, d: duration 39 | var t = distance 40 | var b = 0 41 | var d = window.screen.availHeight // 允许拖拽的最大距离 42 | var c = d / 2.5 // 提示标签最大有效拖拽距离 43 | 44 | return c * Math.sin(t / d * (Math.PI / 2)) + b 45 | } 46 | canRefresh() { 47 | return ( 48 | this.props.onRefresh && 49 | [STATS.refreshing, STATS.loading].indexOf(this.state.loaderState) < 0 50 | ) 51 | } 52 | 53 | touchStart(e) { 54 | if (!this.canRefresh()) return 55 | if (e.touches.length === 1) 56 | this._initialTouch = { 57 | clientY: e.touches[0].clientY, 58 | scrollTop: this.refs.panel.scrollTop 59 | } 60 | } 61 | touchMove(e) { 62 | if (!this.canRefresh()) return 63 | var scrollTop = this.refs.panel.scrollTop 64 | var distance = this.calculateDistance(e.touches[0]) 65 | 66 | if (distance > 0 && scrollTop <= 0) { 67 | var pullDistance = distance - this._initialTouch.scrollTop 68 | if (pullDistance < 0) { 69 | // 修复 webview 滚动过程中 touchstart 时计算panel.scrollTop不准 70 | pullDistance = 0 71 | this._initialTouch.scrollTop = distance 72 | } 73 | var pullHeight = this.easing(pullDistance) 74 | if (pullHeight) e.preventDefault() // 减弱滚动 75 | 76 | this.setState({ 77 | loaderState: 78 | pullHeight > this.props.distanceToRefresh 79 | ? STATS.enough 80 | : STATS.pulling, 81 | pullHeight: pullHeight 82 | }) 83 | } 84 | } 85 | touchEnd() { 86 | if (!this.canRefresh()) return 87 | var endState = { 88 | loaderState: STATS.reset, 89 | pullHeight: 0 90 | } 91 | 92 | if (this.state.loaderState === STATS.enough) { 93 | // refreshing 94 | this.setState({ 95 | loaderState: STATS.refreshing, 96 | pullHeight: 0 97 | }) 98 | 99 | // trigger refresh action 100 | this.props.onRefresh( 101 | () => { 102 | // resolve 103 | this.setState({ 104 | loaderState: STATS.refreshed, 105 | pullHeight: 0 106 | }) 107 | }, 108 | () => { 109 | // reject 110 | this.setState(endState) // reset 111 | } 112 | ) 113 | } else this.setState(endState) // reset 114 | } 115 | 116 | loadMore() { 117 | this.setState({ 118 | loaderState: STATS.loading 119 | }) 120 | this.props.onLoadMore(() => { 121 | // resolve 122 | this.setState({ 123 | loaderState: STATS.init 124 | }) 125 | }) 126 | } 127 | onScroll(e) { 128 | if ( 129 | this.props.autoLoadMore && 130 | this.props.hasMore && 131 | this.state.loaderState !== STATS.loading 132 | ) { 133 | let panel = e.currentTarget 134 | let scrollBottom = 135 | panel.scrollHeight - panel.clientHeight - panel.scrollTop 136 | 137 | if (scrollBottom < 5) this.loadMore() 138 | } 139 | } 140 | 141 | componentWillReceiveProps(nextProps) { 142 | if (nextProps.initializing < 2) 143 | this.setState({ 144 | progressed: 0 // reset progress animation state 145 | }) 146 | } 147 | animationEnd() { 148 | var newState = {} 149 | 150 | if (this.state.loaderState === STATS.refreshed) 151 | newState.loaderState = STATS.init 152 | if (this.props.initializing > 1) newState.progressed = 1 153 | 154 | this.setState(newState) 155 | } 156 | render() { 157 | const { className, hasMore, initializing } = this.props 158 | const { loaderState, pullHeight, progressed } = this.state 159 | 160 | var footer = hasMore ? ( 161 |
    162 |
    163 |
    164 | 165 |
    166 |
    167 | ) : null 168 | 169 | var style = pullHeight 170 | ? { 171 | WebkitTransform: `translate3d(0,${pullHeight}px,0)` 172 | } 173 | : null 174 | 175 | var progressClassName = '' 176 | if (!progressed) { 177 | if (initializing > 0) progressClassName += ' tloader-progress' 178 | if (initializing > 1) progressClassName += ' ed' 179 | } 180 | 181 | return ( 182 |
    this.onScroll(e)} 186 | onTouchStart={e => this.touchStart(e)} 187 | onTouchMove={e => this.touchMove(e)} 188 | onTouchEnd={e => this.touchEnd(e)} 189 | onAnimationEnd={e => this.animationEnd(e)} 190 | > 191 |
    192 |
    193 | 194 |
    195 |
    196 | 197 |
    198 |
    199 |
    200 | {this.props.children} 201 |
    202 | {footer} 203 |
    204 | ) 205 | } 206 | } 207 | 208 | Tloader.defaultProps = { 209 | distanceToRefresh: 60, 210 | autoLoadMore: 1 211 | } 212 | 213 | export default Tloader 214 | -------------------------------------------------------------------------------- /src/component/plugin/touch-loadmore/touch-loader.less: -------------------------------------------------------------------------------- 1 | @bg-dark: #EFEFF4; 2 | @progress-color: #08BF06; 3 | 4 | @height: 3rem; 5 | @fontSize: 11px; 6 | @fontColor: darken(@bg-dark, 40%);// state hint 7 | @btnColor: darken(@bg-dark, 60%);// load more 8 | 9 | @pullingMsg: '下拉刷新'; 10 | @pullingEnoughMsg: '松开刷新'; 11 | @refreshingMsg: '正在刷新...'; 12 | @refreshedMsg: '刷新成功'; 13 | @loadingMsg: '正在加载...'; 14 | @btnLoadMore: '点击加载更多'; 15 | @transition-duration: .2s; 16 | 17 | .tloader-msg:after{ 18 | content: @pullingMsg; 19 | 20 | .state-pulling.enough &{ 21 | content: @pullingEnoughMsg; 22 | } 23 | 24 | .state-refreshed &{ 25 | content: @refreshedMsg; 26 | } 27 | } 28 | .tloader-loading:after{ 29 | content: @loadingMsg; 30 | 31 | .tloader-symbol &{ 32 | content: @refreshingMsg; 33 | } 34 | } 35 | .tloader-btn:after{ 36 | content: @btnLoadMore; 37 | } 38 | 39 | 40 | .tloader{ 41 | position: relative; 42 | 43 | &.state-pulling{ 44 | overflow-y: hidden;// 拖拽时临时阻止ios的overscroll 45 | } 46 | } 47 | 48 | // pull to refresh 49 | .tloader-symbol{ 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | right: 0; 54 | color: @fontColor; 55 | text-align: center; 56 | height: @height; 57 | overflow: hidden; 58 | 59 | // 隐藏刷新提示标签 60 | .state- &, .state-reset &{ 61 | height: 0; 62 | } 63 | // 延迟至reset完成,隐藏刷新提示标签 64 | .state-reset &{ 65 | transition: height 0s @transition-duration; 66 | } 67 | 68 | // hide when loading more 69 | .state-loading &{ 70 | display: none; 71 | } 72 | } 73 | 74 | // 拖拽提示信息 75 | .tloader-msg{ 76 | line-height: @height; 77 | font-size: @fontSize; 78 | 79 | i{ 80 | // arrow down icon 81 | .state-pulling &, .state-reset &{ 82 | display: inline-block; 83 | font-size: 2em; 84 | margin-right: .6em; 85 | vertical-align: middle; 86 | height: 1em; 87 | border-left: 1px solid; 88 | position: relative; 89 | transition: transform .3s ease; 90 | 91 | &:before, &:after{ 92 | content: ''; 93 | position: absolute; 94 | font-size: .5em; 95 | width: 1em; 96 | bottom: 0px; 97 | border-top: 1px solid; 98 | } 99 | &:before{ 100 | right: 1px; 101 | transform: rotate(50deg); 102 | transform-origin: right; 103 | } 104 | &:after{ 105 | left: 0px; 106 | transform: rotate(-50deg); 107 | transform-origin: left; 108 | } 109 | } 110 | 111 | // arrow up 112 | .state-pulling.enough &{ 113 | transform: rotate(180deg); 114 | } 115 | } 116 | 117 | // 刷新中,隐藏提示信息 以切换至loading动画 118 | .state-refreshing &{ 119 | height: 0; 120 | opacity: 0; 121 | } 122 | // 刷新成功提示消息 123 | .state-refreshed &{ 124 | opacity: 1; 125 | transition: opacity 1s; 126 | 127 | // √ icon 128 | i{ 129 | display: inline-block; 130 | box-sizing: content-box; 131 | vertical-align: middle; 132 | margin-right: 10px; 133 | font-size: 20px; 134 | height: 1em; 135 | width: 1em; 136 | border: 1px solid; 137 | border-radius: 100%; 138 | position: relative; 139 | 140 | &:before{ 141 | content: ''; 142 | position: absolute; 143 | top: 3px; 144 | left: 7px; 145 | height: 11px; 146 | width: 5px; 147 | border: solid; 148 | border-width: 0 1px 1px 0; 149 | transform: rotate(40deg); 150 | } 151 | } 152 | } 153 | } 154 | 155 | .tloader-body{ 156 | // active the scrollbar of ios 157 | margin-top: -1px; 158 | padding-top: 1px; 159 | 160 | .state-refreshing &{ 161 | transform: translate3d(0,@height,0); 162 | transition: transform @transition-duration; 163 | } 164 | 165 | .state-refreshed &{ 166 | // handle resolve within .3s 167 | animation: refreshed @transition-duration*2; 168 | } 169 | 170 | .state-reset &{ 171 | transition: transform @transition-duration; 172 | } 173 | } 174 | @keyframes refreshed { 175 | 0%{transform: translate3d(0,@height,0);} 176 | 50%{transform: translate3d(0,@height,0);} 177 | } 178 | 179 | // touch to load more 180 | .tloader-footer{ 181 | .state-refreshing &{ 182 | display: none; 183 | } 184 | 185 | .tloader-btn{ 186 | color: @btnColor; 187 | font-size: .9em; 188 | text-align: center; 189 | line-height: 3rem; 190 | 191 | .state-loading &{ 192 | display: none; 193 | } 194 | } 195 | } 196 | 197 | .tloader-loading{ 198 | display: none; 199 | text-align: center; 200 | line-height: @height; 201 | font-size: @fontSize; 202 | color: @fontColor; 203 | 204 | .ui-loading{ 205 | font-size: 20px; 206 | margin-right: .6rem; 207 | } 208 | 209 | .state-refreshing .tloader-symbol &, .state-loading .tloader-footer &{ 210 | display: block; 211 | } 212 | } 213 | 214 | // loading效果 215 | @keyframes circle { 216 | 100% { transform: rotate(360deg); } 217 | } 218 | .ui-loading{ 219 | display: inline-block; 220 | vertical-align: middle; 221 | font-size: 1.5rem; 222 | width: 1em; 223 | height: 1em; 224 | border: 2px solid darken(@bg-dark, 30%); 225 | border-top-color: #fff; 226 | border-radius: 100%; 227 | animation: circle .8s infinite linear; 228 | 229 | #ui-waiting &{ 230 | border: 2px solid #fff; 231 | border-top-color: darken(@bg-dark, 30%); 232 | } 233 | } 234 | 235 | // 进度条加载效果 236 | @keyframes tloader-progressing { 237 | 0% { width: 0; } 238 | 10%{ width: 40%; } 239 | 20%{ width: 75%; } 240 | 30%{ width: 95%; } 241 | } 242 | @keyframes tloader-progressed { 243 | 0% { 244 | opacity: 1; 245 | } 246 | } 247 | .tloader-progress { 248 | position: relative; 249 | 250 | &:before{ 251 | content: ""; 252 | z-index: 1000; 253 | position: absolute; 254 | top: 0; 255 | left: 0; 256 | height: 2px; 257 | background-color: @progress-color; 258 | width: 99%; 259 | animation: tloader-progressing 9s ease-out; 260 | 261 | .ed&{ 262 | opacity: 0; 263 | width: 100%; 264 | animation: tloader-progressed 1s; 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Router, HashRouter } from 'react-router-dom' 4 | import { createStore, applyMiddleware } from 'redux' 5 | import { Provider } from 'react-redux' 6 | import thunk from 'redux-thunk' 7 | 8 | import reducers from '@store/reducer/ReducerRoot' 9 | import history from '@script/history' 10 | import '@script/firstload' 11 | import '@script/reuse' 12 | import * as serviceWorker from './serviceWorker' 13 | 14 | import App from '@view/App' 15 | import '@assets/sass/global.scss' 16 | 17 | const store = createStore(reducers, applyMiddleware(thunk)) 18 | //Router 发布模式刷新会找不到页面,需配置服务端 19 | const ModeRouter = ({ children }) => { 20 | return Object.is(process.env.NODE_ENV, 'development') ? ( 21 | {children} 22 | ) : ( 23 | {children} 24 | ) 25 | } 26 | 27 | render( 28 | 29 | 30 | 31 | 32 | , 33 | document.getElementById('root') 34 | ) 35 | serviceWorker.unregister() 36 | -------------------------------------------------------------------------------- /src/script/firstload.js: -------------------------------------------------------------------------------- 1 | ({ 2 | el: document.documentElement, 3 | init() { 4 | this.fontAdapt() 5 | window.onresize = () => this.fontAdapt() 6 | }, 7 | fontAdapt() { 8 | const w = this.el.offsetWidth 9 | const fz = w / 20 10 | this.el.style.fontSize = (fz < 20 ? fz : 20) + 'px' 11 | } 12 | }.init()) 13 | -------------------------------------------------------------------------------- /src/script/history.js: -------------------------------------------------------------------------------- 1 | // import createHistory from 'history/createBrowserHistory' 2 | // export default createHistory() 3 | import { createBrowserHistory } from 'history' 4 | export default createBrowserHistory() 5 | -------------------------------------------------------------------------------- /src/script/reuse.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | import { getStorage, showMsg } from '@script/utils' 4 | const isDev = Object.is(process.env.NODE_ENV, 'development') 5 | const apiUrl = 'https://cnodejs.org/api/v1/' 6 | /** 7 | * 公共请求数据 8 | * @param {Object} options 9 | * @returns 10 | */ 11 | export const request = options => { 12 | options.method = options.method || 'get' 13 | const sendData = Object.is(options.method.toLowerCase(), 'post') 14 | ? { 15 | data: options.data 16 | } 17 | : {} 18 | return new Promise((resolve, reject) => { 19 | axios({ 20 | method: options.method, 21 | url: `${apiUrl}${options.url}`, 22 | params: options.params || {}, 23 | ...sendData 24 | }) 25 | .then(response => { 26 | const result = response.data 27 | options.success && options.success(result) 28 | resolve(result) 29 | }) 30 | .catch(error => { 31 | isDev && console.error(error) 32 | if (error.response) { 33 | const { data, status } = error.response 34 | options.error && options.error(data, status) 35 | !data.success && data.error_msg && showMsg(data.error_msg) 36 | reject(data, status) 37 | } 38 | }) 39 | }) 40 | } 41 | 42 | Object.assign(React.Component.prototype, { 43 | $request: request, 44 | $showMsg: showMsg, 45 | $getStorage: getStorage 46 | }) 47 | -------------------------------------------------------------------------------- /src/script/routers.js: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable' 2 | 3 | /* const ShowLoadingComponent = props => { 4 | if (props.isLoading) { 5 | if (props.timedOut) { 6 | return
    加载超时
    7 | } 8 | return props.pastDelay ?
    玩命加载中...
    : null 9 | } 10 | return props.error ?
    Error! Component failed to load
    : null 11 | } */ 12 | const AsyncLoadable = opts => { 13 | return Loadable({ 14 | loading: () => null, //ShowLoadingComponent, 15 | delay: 200, 16 | timeout: 1000, 17 | ...opts 18 | }) 19 | } 20 | 21 | export default [{ 22 | path: '/', 23 | exact: true, 24 | component: AsyncLoadable({ 25 | loader: () => 26 | import ('@view/home/Home') 27 | }), 28 | name: 'NodeJS论坛' 29 | }, 30 | { 31 | path: '/index/article-details', 32 | component: AsyncLoadable({ 33 | loader: () => 34 | import ('@view/home/Article-Details') 35 | }), 36 | name: '详情' 37 | }, 38 | { 39 | path: '/news', 40 | exact: true, 41 | component: AsyncLoadable({ 42 | loader: () => 43 | import ('@view/info/News') 44 | }), 45 | name: '消息' 46 | }, 47 | { 48 | path: '/share', 49 | component: AsyncLoadable({ 50 | loader: () => 51 | import ('@view/pose/Issue') 52 | }), 53 | name: '分享' 54 | }, 55 | { 56 | path: '/user', 57 | exact: true, 58 | component: AsyncLoadable({ 59 | loader: () => 60 | import ('@view/personal/Mine') 61 | }), 62 | name: '我的' 63 | }, 64 | { 65 | path: '/user/login', 66 | component: AsyncLoadable({ 67 | loader: () => 68 | import ('@component/common/SharedCompt').then(module => module.UserLogin) 69 | }), 70 | name: '登录' 71 | }, 72 | { 73 | component: AsyncLoadable({ 74 | loader: () => 75 | import ('@view/NotFound') 76 | }), 77 | name: '未找到' 78 | } 79 | ] 80 | -------------------------------------------------------------------------------- /src/script/utils.js: -------------------------------------------------------------------------------- 1 | import Rstore from 'store' 2 | 3 | /** 4 | * 算时间差 5 | * @param {Date} hisTime 历史时间 6 | * @param {Date} nowTime 当前时间 7 | * @returns 8 | */ 9 | 10 | export const formatDate = (hisTime, nowTime = new Date()) => { 11 | let result = null 12 | const diffValue = +new Date(nowTime) - +new Date(hisTime) 13 | const minute = 1000 * 60 14 | const hour = minute * 60 15 | const day = hour * 24 16 | // const halfamonth = day * 15 17 | const month = day * 30 18 | const year = month * 12 19 | 20 | const [_year, _month, _week, _day, _hour, _min] = [ 21 | diffValue / year, 22 | diffValue / month, 23 | diffValue / (7 * day), 24 | diffValue / day, 25 | diffValue / hour, 26 | diffValue / minute 27 | ] 28 | if (_year >= 1) result = parseInt(_year, 10) + '年前' 29 | else if (_month >= 1) result = parseInt(_month, 10) + '个月前' 30 | else if (_week >= 1) result = parseInt(_week, 10) + '周前' 31 | else if (_day >= 1) result = parseInt(_day, 10) + '天前' 32 | else if (_hour >= 1) result = parseInt(_hour, 10) + '个小时前' 33 | else if (_min >= 1) result = parseInt(_min, 10) + '分钟前' 34 | else result = '刚刚' 35 | return result 36 | } 37 | /** 38 | * 提示弹窗 39 | * @param {String} msg 提示信息 40 | */ 41 | let tipTime = null 42 | export const showMsg = msg => { 43 | tipTime && clearTimeout(tipTime) 44 | const formMsg = document.getElementById('form-msg') 45 | if (formMsg) { 46 | formMsg.style.display = 'block' 47 | formMsg.innerHTML = 48 | "
    " + msg + '
    ' 49 | } else { 50 | const sDiv = document.createElement('div') 51 | sDiv.id = 'form-msg' 52 | sDiv.className = 'tc form-msg' 53 | sDiv.innerHTML = 54 | "
    " + msg + '
    ' 55 | document.getElementsByTagName('body')[0].appendChild(sDiv) 56 | } 57 | tipTime = setTimeout( 58 | () => (document.getElementById('form-msg').style.display = 'none'), 59 | 1000 60 | ) 61 | } 62 | /** 63 | * 存储数据 64 | * @param {String} key 名称 65 | * @param {Object} value 数据 66 | */ 67 | export const setStorage = (key, value) => { 68 | value['setKeyTime'] = +new Date() 69 | Rstore.set(key, value) 70 | } 71 | 72 | /** 73 | * 获取用户存储信息 74 | * 75 | * @returns 76 | */ 77 | export const getStorage = () => { 78 | const userInfo = Rstore.get('USER_INFO') 79 | if (userInfo && userInfo.accesstoken) { 80 | return userInfo 81 | } 82 | return false 83 | } 84 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/store/actions/ActionArticle.js: -------------------------------------------------------------------------------- 1 | import { request } from '@script/reuse' 2 | export default { 3 | fetchData(id) { 4 | return (dispatch, getState) => { 5 | dispatch(this.fetchStart()) 6 | //获取文章详情 7 | request({ 8 | url: `topic/${id}`, 9 | success: result => { 10 | dispatch(this.fetchDone(result)) 11 | }, 12 | error: err => { 13 | dispatch(this.fetchFail(err)) 14 | } 15 | }) 16 | } 17 | }, 18 | fetchStart() { 19 | return { type: 'FETCH_START' } 20 | }, 21 | fetchDone(result) { 22 | return { type: 'FETCH_DONE', result: result } 23 | }, 24 | fetchFail(err) { 25 | return { type: 'FETCH_Fail', result: err } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/store/actions/ActionNews.js: -------------------------------------------------------------------------------- 1 | import { request } from '@script/reuse' 2 | export default { 3 | fetchData(accesstoken) { 4 | return (dispatch, getState) => { 5 | //获取消息条数 6 | request({ 7 | url: 'message/count', 8 | params: { 9 | accesstoken 10 | }, 11 | success: result => { 12 | dispatch(this.fetchDone(result)) 13 | } 14 | }) 15 | } 16 | }, 17 | fetchDone(result) { 18 | return { type: 'UPDATE_NEWS', result: result } 19 | // return { 20 | // type: 'UPDATE_NEWS', 21 | // result: { 22 | // success: true, 23 | // data: 2 24 | // } 25 | // } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/store/actions/ActionReply.js: -------------------------------------------------------------------------------- 1 | import { request } from '@script/reuse' 2 | export default { 3 | fetchData(opts, callback) { 4 | // opts.content = `${opts.content}

    FROMCNODE

    `; 6 | return (dispatch, getState) => { 7 | const topicId = getState().Article.result.data.id 8 | ;(async () => { 9 | const getresult = await request({ 10 | method: 'post', 11 | url: `/topic/${topicId}/replies`, 12 | data: opts 13 | }).catch(() => { 14 | this.$showMsg('亲,评论失败!') 15 | }) 16 | if (getresult) { 17 | //评论后刷新 18 | request({ 19 | url: `topic/${topicId}`, 20 | success: result => { 21 | dispatch(this.fetchDone(result)) 22 | callback && callback() 23 | } 24 | }) 25 | } 26 | })() 27 | } 28 | }, 29 | fetchDone(result) { 30 | return { type: 'UPDATE_REPLY', result: result } 31 | }, 32 | sendCnt(data) { 33 | return { type: 'GET_REPLY_CNT', reply_content: data } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/store/actions/ActionTopicList.js: -------------------------------------------------------------------------------- 1 | export default { 2 | keepTopic: res => (dispatch, getState) => { 3 | const { page, data, type, isReset } = res 4 | return dispatch( 5 | (() => { 6 | return { 7 | type: 'SAVE_TOPIC', 8 | page, 9 | topicType: type, 10 | result: isReset ? data : getState().rTopicList.result.concat(data) 11 | } 12 | })() 13 | ) 14 | }, 15 | recordScrollSite: scrollTop => (dispatch, getState) => { 16 | return dispatch( 17 | (() => { 18 | return { 19 | type: 'SCROLL_TOP_SITE', 20 | scrollTop 21 | } 22 | })() 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/store/reducer/ReduceTopicList.js: -------------------------------------------------------------------------------- 1 | export default ( 2 | state = { 3 | result: [], 4 | page: 0, 5 | scrollTop: 0, 6 | topicType: 'all' 7 | }, 8 | action 9 | ) => { 10 | switch (action.type) { 11 | case 'SAVE_TOPIC': 12 | return { 13 | ...state, 14 | result: action.result, 15 | page: action.page, 16 | topicType: action.topicType 17 | } 18 | case 'SCROLL_TOP_SITE': 19 | return { 20 | ...state, 21 | scrollTop: action.scrollTop 22 | } 23 | default: 24 | return state 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/store/reducer/ReducerArticle.js: -------------------------------------------------------------------------------- 1 | export default ( 2 | state = { 3 | isLoad: true 4 | }, 5 | action 6 | ) => { 7 | switch (action.type) { 8 | case 'FETCH_START': 9 | return { 10 | ...state, 11 | isLoad: true 12 | } 13 | case 'FETCH_DONE': 14 | return { isLoad: false, result: action.result } 15 | case 'FETCH_Fail': 16 | return { 17 | ...state, 18 | isLoad: false 19 | } 20 | case 'UPDATE_REPLY': 21 | return { 22 | ...state, 23 | result: action.result 24 | } 25 | case 'GET_REPLY_CNT': 26 | state.result.data.reply_content = action.reply_content 27 | return { 28 | ...state, 29 | result: state.result 30 | } 31 | default: 32 | return state 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/store/reducer/ReducerNews.js: -------------------------------------------------------------------------------- 1 | export default (state = {}, action) => { 2 | switch (action.type) { 3 | case 'UPDATE_NEWS': 4 | return { result: action.result } 5 | default: 6 | return state 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/store/reducer/ReducerRoot.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import Article from './ReducerArticle' 3 | import ReadNews from './ReducerNews' 4 | import rTopicList from './ReduceTopicList' 5 | export default combineReducers({ Article, ReadNews, rTopicList }) 6 | -------------------------------------------------------------------------------- /src/view/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Switch, Route, Redirect } from 'react-router-dom' 3 | import { BackTop } from 'antd' 4 | import classNames from 'classnames' 5 | import qs from 'qs' 6 | 7 | import { getStorage } from '@script/utils' 8 | import RouteConfig from '@script/routers' 9 | 10 | import Menu from '@component/Menu' 11 | import SubHeader from '@component/SubHeader' 12 | 13 | import '@fonts/svg/gotop.svg' 14 | 15 | const RouteWithSubRoutes = route => ( 16 | } 20 | /> 21 | ) 22 | const findMatch = path => RouteConfig.find(f => f.path === path) 23 | 24 | export default () => { 25 | let isUserCur = true 26 | let queryName = null 27 | const arrPathName = ['/index/article-details', '/', '/news', '/user'] 28 | return ( 29 | { 31 | const { pathname, search } = location 32 | const getRoutes = findMatch(pathname) 33 | const isBackTop = arrPathName.includes(pathname) 34 | const isAdd = getRoutes && pathname.split('/').length < 3 35 | const classMain = classNames('tl0 w100 view-main', { 36 | 'pa h100': Object.is(pathname, '/'), 37 | container: isAdd, 38 | 'container-top': !isAdd, 39 | 'bg-white': Object.is(pathname, '/share') 40 | }) 41 | //当前用户 42 | if (Object.is(pathname, '/user')) { 43 | const userInfo = getStorage() 44 | queryName = qs.parse(search.slice(1)).name 45 | isUserCur = userInfo && Object.is(userInfo.loginname, queryName) 46 | } 47 | return ( 48 |
    49 | {getRoutes && ( 50 | 54 | )} 55 | 56 | {RouteConfig.map((route, index) => ( 57 | 58 | ))} 59 | 60 | 61 | {isAdd && isUserCur && } 62 | {isBackTop && ( 63 | 68 | 69 | 70 | 71 | 72 | )} 73 |
    74 | ) 75 | }} 76 | /> 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/view/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import '@fonts/svg/notfound.svg' 4 | export default () => { 5 | return ( 6 |
    7 | 8 | 9 | 10 | 您找的页面到了火星,送我 15 | 回地球! 16 | 17 |
    18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/view/home/Article-Comment.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import LazyImage from '@component/common/LazyImage' 4 | import classNames from 'classnames' 5 | 6 | import '@fonts/svg/thumbs.svg' 7 | import '@fonts/svg/comment.svg' 8 | class CommentList extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | isLogin: this.$getStorage(), 13 | userTotalUps: this.props.item.ups 14 | } 15 | } 16 | UserLike(loginname, id, i) { 17 | //用户点赞 18 | const { isLogin } = this.state 19 | const uid = isLogin && isLogin.id 20 | if (!isLogin) { 21 | return this.props.history.push('/user/login') 22 | } else if (isLogin && isLogin.loginname === loginname) { 23 | return this.$showMsg('亲,不能给自己点赞!') 24 | } 25 | this.$request({ 26 | method: 'post', 27 | url: `/reply/${id}/ups`, 28 | data: { 29 | accesstoken: isLogin.accesstoken 30 | }, 31 | success: result => { 32 | let curUps = this.props.item.ups 33 | if (result.success) { 34 | if (result.action === 'down') { 35 | //取消点赞 36 | curUps.map((item, index) => { 37 | return item === uid && curUps.splice(index, 1) 38 | }) 39 | } else { 40 | curUps.push(uid) 41 | } 42 | this.setState({ userTotalUps: curUps }) 43 | } 44 | } 45 | }) 46 | } 47 | userIsUp(arr) { 48 | //是否点赞 49 | const id = this.state.isLogin ? this.state.isLogin.id : '' 50 | for (let i = 0, len = arr.length; i < len; i++) { 51 | if (arr[i] === id) return true 52 | } 53 | return false 54 | } 55 | replyTopic(name, reply_id, isAtmuch) { 56 | //用户评论 57 | const { isLogin } = this.state 58 | if (!isLogin) { 59 | return this.props.history.push('/user/login') 60 | } 61 | this.props.sendcnt({ name, reply_id }) 62 | } 63 | render() { 64 | const { author, content, ups, id } = this.props.item 65 | let index = this.props.index 66 | const createMarkup = () => { 67 | return { __html: content } 68 | } 69 | const isUp = this.userIsUp(ups) 70 | return ( 71 |
  • 79 |
    80 | 86 | 91 | 92 |

    {++index}楼

    93 |
    94 |
    95 |

    96 | {author.loginname} 97 |

    98 |
    102 |
    103 | 104 | 108 | 109 | 110 | {ups.length ? ups.length : ''} 111 | 112 | 116 | 117 | 118 |
    119 |
    120 |
  • 121 | ) 122 | } 123 | } 124 | 125 | export default class ArticleComment extends React.Component { 126 | componentDidMount() { 127 | this.props.getElHeight(this.refs.replyList.scrollHeight) 128 | } 129 | render() { 130 | const { replies, reply_count } = this.props.info 131 | return ( 132 |
    133 |
    138 | 139 | 140 | 141 |

    142 | 共{reply_count}条评论! 143 |

    144 |
    145 |
      146 | {replies.map((item, index) => { 147 | return ( 148 | 155 | ) 156 | })} 157 |
    158 |
    159 | ) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/view/home/Article-DecCnt.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | 5 | import ActionReply from '@store/actions/ActionReply' 6 | import ArticleItem from '@component/ArticleItem' 7 | import { NotLogin } from '@component/common/SharedCompt' 8 | import MaskPopups from '@component/common/MaskPopups' 9 | 10 | import ArticleComment from './Article-Comment' 11 | import UserComment from './User-Comment' 12 | 13 | import '@fonts/svg/operat-del.svg' 14 | import '@fonts/svg/operat-keep-default.svg' 15 | import '@fonts/svg/operat-keep-active.svg' 16 | import '@fonts/svg/meedit.svg' 17 | 18 | /* class ThemeKeep extends React.Component { 19 | constructor(props) { 20 | super(props) 21 | this.state = { 22 | is_collect: props.isKeep 23 | } 24 | console.log(props.isKeep) 25 | } 26 | topicCollect(id, is_keep) { 27 | console.log(is_keep) 28 | if (is_keep) return this.userWithCollect(id, 'collect') //收藏 29 | return this.userWithCollect(id, 'de_collect') //取消收藏 30 | } 31 | userWithCollect(id, url, callback) { 32 | const isLogin = this.$getStorage() 33 | 34 | isLogin && 35 | this.$request({ 36 | method: 'post', 37 | url: `topic_collect/${url}`, 38 | data: { 39 | accesstoken: isLogin.accesstoken, 40 | topic_id: id 41 | }, 42 | success: result => { 43 | result.success && this.setState({ is_collect: true }) 44 | } 45 | }) 46 | } 47 | render() { 48 | const { topicId } = this.props 49 | return ( 50 |
    51 | {this.state.is_collect ? ( 52 | 53 | 54 | 55 | 56 | 取消收藏 57 | 58 | ) : ( 59 | 60 | 61 | 62 | 63 | 收藏 64 | 65 | )} 66 |
    67 | ) 68 | } 69 | } */ 70 | 71 | class ArticleDecCnt extends React.Component { 72 | constructor(props) { 73 | super(props) 74 | this.state = { 75 | isPopups: false, 76 | listh: 0 77 | } 78 | this.userInfo = this.$getStorage() 79 | } 80 | SendCancelRun() { 81 | this.setState({ isPopups: false }) 82 | } 83 | SendDeterMine(id) { 84 | this.$request({ 85 | method: 'post', 86 | url: `topic_collect/de_collect`, 87 | data: { 88 | accesstoken: this.userInfo.accesstoken, 89 | topic_id: this.props.info.data.id 90 | }, 91 | success: result => { 92 | // this .props .history .push({pathname: '/'}); 93 | } 94 | }) 95 | } 96 | getElHeight(h) { 97 | this.setState({ listh: h }) 98 | } 99 | render() { 100 | const { data } = this.props.info 101 | const createMarkup = () => { 102 | return { __html: data.content } 103 | } 104 | const isLogin = this.userInfo 105 | return ( 106 |
    112 | 113 | {/* {isLogin &&
    114 | {data.author_id === isLogin.id 115 | ?
    116 |
    117 | 118 | 119 | 120 | 编辑 121 |
    122 |
    { 124 | this.setState({isPopups: true}) 125 | }}> 126 | 127 | 128 | 129 | 删除 130 |
    131 | 132 |
    133 | :
    134 | 135 |
    136 | } 137 |
    } */} 138 |
    142 | 147 |
    156 | {isLogin ? ( 157 | 163 | ) : ( 164 | 165 | )} 166 |
    167 | {this.state.isPopups && ( 168 | 173 | )} 174 |
    175 | ) 176 | } 177 | } 178 | 179 | const mapStateToProps = state => { 180 | return { Article: state.Article } 181 | } 182 | const actions = { 183 | ...ActionReply 184 | } 185 | const mapDispatchToProps = dispatch => ({ 186 | actions: bindActionCreators(actions, dispatch) 187 | }) 188 | export default connect(mapStateToProps, mapDispatchToProps)(ArticleDecCnt) 189 | -------------------------------------------------------------------------------- /src/view/home/Article-Details.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import qs from 'qs' 5 | 6 | import ActionArticle from '@store/actions/ActionArticle' 7 | 8 | import ArticleDecCnt from './Article-DecCnt' 9 | import { LoadLoop, LoadFail } from '@component/common/SharedCompt' 10 | 11 | import 'github-markdown-css' 12 | class ArticleDeatils extends React.Component { 13 | constructor(props) { 14 | super(props) 15 | this.props.actions.fetchData( 16 | qs.parse(this.props.location.search.slice(1)).id 17 | ) 18 | } 19 | render() { 20 | const { isLoad } = this.props.Article 21 | return isLoad ? ( 22 | 23 | ) : this.props.Article.result && this.props.Article.result.success ? ( 24 | 25 | ) : ( 26 | 27 | ) 28 | } 29 | } 30 | const mapStateToProps = state => { 31 | return { Article: state.Article } 32 | } 33 | const actions = { 34 | ...ActionArticle 35 | } 36 | const mapDispatchToProps = dispatch => ({ 37 | actions: bindActionCreators(actions, dispatch) 38 | }) 39 | export default connect(mapStateToProps, mapDispatchToProps)(ArticleDeatils) 40 | -------------------------------------------------------------------------------- /src/view/home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NavLink } from 'react-router-dom' 3 | import qs from 'qs' 4 | 5 | import LoadMoreInfo from '@component/plugin/touch-loadmore/LoadMore' 6 | 7 | const arrTabHead = [ 8 | { 9 | name: '全部', 10 | link: '/', 11 | type: 'all' 12 | }, 13 | { 14 | name: '精华', 15 | link: '/?tab=good', 16 | type: 'good' 17 | }, 18 | { 19 | name: '分享', 20 | link: '/?tab=share', 21 | type: 'share' 22 | }, 23 | { 24 | name: '问答', 25 | link: '/?tab=ask', 26 | type: 'ask' 27 | }, 28 | { 29 | name: '招聘', 30 | link: '/?tab=job', 31 | type: 'job' 32 | }, 33 | { 34 | name: '测试', 35 | link: '/?tab=dev', 36 | type: 'dev' 37 | } 38 | ] 39 | 40 | /* const ListPhoto = props => { 41 | const tabName = props.text || [] 42 | const dataItem = tabName.length 43 | ? tabName.map((item, index) => ( 44 |
  • 45 | {index} 46 | {item.slide_name} 47 |
  • 48 | )) 49 | : '加载中...' 50 | return
      {dataItem}
    51 | } */ 52 | const TabNav = props => { 53 | const activeTab = {} 54 | activeTab[props.type] = 'ft-color' 55 | return ( 56 | 71 | ) 72 | } 73 | 74 | export default class Home extends React.Component { 75 | componentDidMount() { 76 | // console.log(this.refs.ArticleList) 77 | } 78 | shouldComponentUpdate(nextProps, nextState) { 79 | const curPath = qs.parse(nextProps.location.search.slice(1)).tab || 'all' 80 | const goPath = qs.parse(this.props.location.search.slice(1)).tab || 'all' 81 | return curPath !== goPath 82 | } 83 | render() { 84 | const tabType = qs.parse(this.props.location.search.slice(1)).tab || 'all' 85 | return ( 86 |
    87 | 88 | 89 |
    90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/view/home/User-Comment.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import '@fonts/svg/send.svg' 4 | export default class UserComment extends React.Component { 5 | constructor(props) { 6 | super(props) 7 | const isLogin = this.$getStorage() 8 | this.state = { 9 | accesstoken: isLogin && isLogin.accesstoken, 10 | content: '', 11 | reply_id: '' 12 | } 13 | } 14 | newCommentSubmit() { 15 | const { state } = this 16 | if (!state.content) { 17 | return this.$showMsg('亲,忘了输内容了!') 18 | } 19 | this.props.actions.fetchData( 20 | { 21 | ...state, 22 | reply_id: this.state.reply_id 23 | }, 24 | () => { 25 | this.setState({ content: '' }) 26 | window.scrollTo( 27 | 0, 28 | document.documentElement.scrollHeight || this.props.listh 29 | ) 30 | } 31 | ) 32 | } 33 | handleChange(e) { 34 | this.setState({ content: e.target.value }) 35 | } 36 | EnterSubmit(e) { 37 | if (e.keyCode === 13) this.newCommentSubmit() 38 | } 39 | /* shouldComponentUpdate(nextProps, nextState) { 40 | const {reply_content} = nextProps.info.data; 41 | if (reply_content) { 42 | const isUpdate = (this.state.value !=== `@${reply_content.name}`) || (this.state.reply_id !=== `${reply_content.reply_id}`); 43 | return isUpdate; 44 | } 45 | return true; 46 | } */ 47 | componentWillReceiveProps(prevProps, prevState) { 48 | const { reply_content } = prevProps.info.data 49 | if (reply_content && this.state.reply_id !== `${reply_content.reply_id}`) { 50 | this.setState({ 51 | content: `@${reply_content.name} `, 52 | reply_id: `${reply_content.reply_id}` 53 | }) 54 | } 55 | } 56 | render() { 57 | return ( 58 |
    59 |
    60 |
    66 | 72 | 77 | 78 |
    79 |
    80 | 91 |
    92 |
    93 | 103 |
    104 |
    105 |
    106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/view/info/News.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { DetectLogin, LoadLoop, NotResult } from '@component/common/SharedCompt' 4 | import { Tabs } from 'antd' 5 | import { formatDate } from '@script/utils' 6 | import { bindActionCreators } from 'redux' 7 | import { connect } from 'react-redux' 8 | 9 | import ActionNews from '@store/actions/ActionNews' 10 | 11 | const TabPane = Tabs.TabPane 12 | 13 | class UserNews extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | this.userInfo = this.$getStorage() 17 | this.state = { 18 | info: {} 19 | } 20 | } 21 | componentDidMount() { 22 | this.fetchData() 23 | } 24 | fetchData() { 25 | this.$request({ 26 | url: `messages`, 27 | params: { 28 | mdrender: false, 29 | accesstoken: this.userInfo.accesstoken 30 | }, 31 | success: result => { 32 | this.setState({ info: result }) 33 | } 34 | }) 35 | } 36 | render() { 37 | const { info } = this.state 38 | return ( 39 |
    40 | {info.data ? ( 41 | 46 | ) : ( 47 | 48 | )} 49 |
    50 | ) 51 | } 52 | } 53 | 54 | class NewsContent extends React.Component { 55 | constructor(props) { 56 | super(props) 57 | this.userInfo = this.$getStorage() 58 | } 59 | clearReadMsg() { 60 | this.$request({ 61 | method: 'post', 62 | url: `message/mark_all`, 63 | data: { 64 | accesstoken: this.userInfo.accesstoken 65 | }, 66 | success: result => { 67 | console.log(result) 68 | this.props.afresh() 69 | this.props.actions.fetchData() 70 | this.userInfo && this.props.actions.fetchData(this.userInfo.accesstoken) 71 | } 72 | }) 73 | } 74 | render() { 75 | const { data } = this.props 76 | const [hasMsgLen, hasNotMsgLen] = [ 77 | data.has_read_messages.length, 78 | data.hasnot_read_messages.length 79 | ] 80 | return ( 81 |
    82 | 83 | 84 | {hasNotMsgLen ? ( 85 | data.hasnot_read_messages.map((item, index) => { 86 | return 87 | }) 88 | ) : ( 89 | 90 | )} 91 | 92 | 93 | {hasMsgLen ? ( 94 | data.has_read_messages.map((item, index) => { 95 | return 96 | }) 97 | ) : ( 98 | 99 | )} 100 | {/*
    101 | 106 |
    */} 107 |
    108 |
    109 |
    110 | ) 111 | } 112 | } 113 | const NewsList = props => { 114 | const { item } = props 115 | const createMarkup = () => { 116 | return { __html: item.reply.content } 117 | } 118 | return ( 119 |
    120 |
    121 | 127 |
    128 |

    133 | 134 | {item.author.loginname} 135 | 136 | {formatDate(item.create_at)} 137 |

    138 |
    139 |
    140 | 在话题 150 | {item.topic.title} 151 | 152 | {item.type === 'at' ? '中@了你!' : '回复了你!'} 153 |
    157 |
    158 |
    159 |
    160 |
    161 |
    162 | ) 163 | } 164 | 165 | // export default DetectLogin({realuser: UserNews}) 166 | const mapStateToProps = state => { 167 | return { ReadNews: state.ReadNews } 168 | } 169 | const actions = { 170 | ...ActionNews 171 | } 172 | const mapDispatchToProps = dispatch => ({ 173 | actions: bindActionCreators(actions, dispatch) 174 | }) 175 | export default connect(mapStateToProps, mapDispatchToProps)( 176 | DetectLogin({ realuser: UserNews }) 177 | ) 178 | -------------------------------------------------------------------------------- /src/view/personal/Mine.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { Tabs } from 'antd' 4 | import Rstore from 'store' 5 | import qs from 'qs' 6 | import classNames from 'classnames' 7 | 8 | import ArticleItem from '@component/ArticleItem' 9 | import LazyImage from '@component/common/LazyImage' 10 | import MaskPopups from '@component/common/MaskPopups' 11 | 12 | import { formatDate } from '@script/utils' 13 | import { LoadLoop, NotResult } from '@component/common/SharedCompt' 14 | 15 | import '@fonts/svg/meuser.svg' 16 | import '@fonts/svg/metime.svg' 17 | import '@fonts/svg/mescore.svg' 18 | import '@fonts/svg/write.svg' 19 | import '@fonts/svg/page-views.svg' 20 | import '@fonts/svg/medropout.svg' 21 | import '@fonts/svg/meedit.svg' 22 | 23 | const checkNum = num => (+num > 10 ? num : `0${num}`) 24 | const dateFormat = time => { 25 | const date = new Date(time) 26 | const [year, month, day] = [ 27 | date.getFullYear(), 28 | date.getMonth() + 1, 29 | date.getDate() 30 | ] 31 | return `${year} / ${checkNum(month)} / ${checkNum(day)}` 32 | } 33 | 34 | class Mine extends React.Component { 35 | constructor(props) { 36 | super(props) 37 | this.userInfo = this.$getStorage() 38 | this.state = { 39 | info: {} 40 | } 41 | this.userName = qs.parse(this.props.location.search.slice(1)).name 42 | if (this.userInfo) { 43 | this.isUserCur = this.userInfo.loginname === this.userName 44 | } 45 | } 46 | async componentDidMount() { 47 | const getUserInfo = await this.$request({ 48 | url: `user/${this.userName}` 49 | // success: (result) => { this.setState({info: result.data}) } 50 | }) 51 | if (getUserInfo) { 52 | this.$request({ 53 | url: `topic_collect/${this.userName}`, 54 | success: result => { 55 | const { info } = this.state 56 | info['collect'] = result.data 57 | const data = Object.assign({}, info, getUserInfo.data) 58 | this.setState({ info: data }) 59 | } 60 | }) 61 | } 62 | } 63 | render() { 64 | const { info } = this.state 65 | return info.loginname ? ( 66 | 72 | ) : ( 73 | 74 | ) 75 | } 76 | } 77 | 78 | const TabPane = Tabs.TabPane 79 | 80 | class UserCenter extends React.Component { 81 | constructor(props) { 82 | super(props) 83 | this.state = { 84 | isPopups: false 85 | } 86 | } 87 | SendCancelRun() { 88 | this.setState({ isPopups: false }) 89 | } 90 | SendDeterMine() { 91 | Rstore.remove('USER_INFO') 92 | this.props.history.push({ pathname: '/' }) 93 | } 94 | render() { 95 | const { result, isUserCur } = this.props 96 | return ( 97 |
    98 |
    99 |
    106 | 112 |
    113 |

    114 | 115 | 116 | 117 | {result.loginname} 118 |

    119 |

    120 | 121 | 122 | 123 | {result.score}分 124 |

    125 |

    126 | 127 | 128 | 129 | {dateFormat(result.create_at)} 130 |

    131 |
    132 | {isUserCur && ( 133 |
    134 |

    135 | 136 | 137 | 138 | 139 | 发布话题 140 | 141 |

    142 |

    { 144 | this.setState({ isPopups: true }) 145 | }} 146 | > 147 | 148 | 149 | 150 | 退出 151 |

    152 |
    153 | )} 154 |
    155 |
    156 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | {result.collect.length ? ( 168 | result.collect.map((item, index) => ( 169 | 175 | )) 176 | ) : ( 177 | 178 | )} 179 | 180 | 181 | {this.state.isPopups && ( 182 | 187 | )} 188 |
    189 | ) 190 | } 191 | } 192 | 193 | const UserTopicList = props => { 194 | const { info } = props 195 | return ( 196 |
    197 | {info.length ? ( 198 | info.map((item, index) => { 199 | return ( 200 |
    201 | 212 | 218 |
    219 |

    {item.title}

    220 |
    225 | 226 | 227 | 228 | 229 | {item.author.loginname} 230 | 231 | {formatDate(item.last_reply_at)} 232 |
    233 |
    234 | 235 |
    236 | ) 237 | }) 238 | ) : ( 239 | 240 | )} 241 |
    242 | ) 243 | } 244 | 245 | export default Mine 246 | -------------------------------------------------------------------------------- /src/view/pose/Issue.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DetectLogin } from '@component/common/SharedCompt' 3 | 4 | class Issue extends React.Component { 5 | constructor(props) { 6 | super(props) 7 | const isLogin = this.$getStorage() 8 | this.state = { 9 | title: '', 10 | content: '', 11 | tab: 'dev', 12 | accesstoken: isLogin && isLogin.accesstoken, 13 | arrRadio: [ 14 | { 15 | type: 'ask', 16 | name: '问答', 17 | isChecked: true 18 | }, 19 | { 20 | type: 'share', 21 | name: '分享', 22 | isChecked: false 23 | }, 24 | { 25 | type: 'job', 26 | name: '招聘', 27 | isChecked: false 28 | }, 29 | { 30 | type: 'dev', 31 | name: '测试', 32 | isChecked: false 33 | } 34 | ] 35 | } 36 | const { state } = this 37 | this.themeTitle = e => { 38 | state.title = e.target.value 39 | } 40 | this.themeContent = e => { 41 | state.content = e.target.value 42 | } 43 | this.issueSubmit = () => { 44 | if (state.title.length < 10) { 45 | return this.$showMsg('亲,请输入10字以上的标题!') 46 | } else if (!state.content) { 47 | return this.$showMsg('亲,请输入内容!') 48 | } 49 | const { accesstoken, title, tab, content } = state 50 | this.$request({ 51 | method: 'post', 52 | url: 'topics', 53 | data: { accesstoken, title, tab, content }, 54 | success: result => { 55 | if (result.success) { 56 | this.$showMsg('发表成功!') 57 | this.props.history.push({ 58 | pathname: '/index/article-details', 59 | search: `?id=${result.topic_id}` 60 | }) 61 | } else { 62 | this.$showMsg('发表失败!') 63 | } 64 | }, 65 | error: () => { 66 | this.$showMsg('发表失败!') 67 | } 68 | }) 69 | } 70 | } 71 | selectThemeCate(i, e) { 72 | //类型选择 73 | const { arrRadio } = this.state 74 | if (arrRadio[i].isChecked) return 75 | arrRadio.map((item, index) => { 76 | return (item.isChecked = Object.is(i, index) ? true : false) 77 | }) 78 | this.setState( 79 | { 80 | tab: arrRadio[i].type, 81 | arrRadio: arrRadio 82 | }, 83 | () => { 84 | // console.log(this.state.tab) 85 | } 86 | ) 87 | } 88 | render() { 89 | return ( 90 |
    91 |
    92 | 99 |
    100 |
    101 |