├── tsconfig.prod.json ├── images.d.ts ├── public ├── favicon.ico ├── img │ └── icons │ │ ├── icon-192x192.png │ │ └── icon-512x512.png ├── manifest.json ├── cus-service-worker.js └── index.html ├── src ├── images │ ├── down.png │ ├── theUi.png │ └── portrait.png ├── store │ ├── types.tsx │ ├── store.tsx │ ├── constants.tsx │ ├── reducers.tsx │ └── actions.tsx ├── index.scss ├── config.ts ├── components │ ├── Icon │ │ ├── Icon.scss │ │ └── Icon.tsx │ ├── Skeleton │ │ ├── SkeletonExp.tsx │ │ └── SkeletonExp.scss │ ├── ExpBrief │ │ ├── ExpBrief.scss │ │ └── ExpBrief.tsx │ └── ExpDetail │ │ ├── ExpDetail.scss │ │ └── ExpDetail.tsx ├── App.test.js ├── views │ ├── SpExp │ │ ├── SpExp.scss │ │ └── SpExp.tsx │ ├── Home │ │ ├── Home.scss │ │ └── Home.tsx │ ├── SelectType │ │ ├── SelectType.scss │ │ └── SelectType.tsx │ └── ExpDetailContainer │ │ └── ExpDetailContainer.tsx ├── index.js ├── App.tsx ├── api │ └── data.ts ├── style │ ├── common.scss │ └── iconfont.js ├── logo.svg └── serviceWorker.js ├── tslint.json ├── .gitignore ├── global.d.ts ├── tsconfig.json ├── README.md ├── config-overrides.js └── package.json /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HolyZheng/browseExpbyReact/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/images/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HolyZheng/browseExpbyReact/HEAD/src/images/down.png -------------------------------------------------------------------------------- /src/images/theUi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HolyZheng/browseExpbyReact/HEAD/src/images/theUi.png -------------------------------------------------------------------------------- /src/images/portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HolyZheng/browseExpbyReact/HEAD/src/images/portrait.png -------------------------------------------------------------------------------- /public/img/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HolyZheng/browseExpbyReact/HEAD/public/img/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/img/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HolyZheng/browseExpbyReact/HEAD/public/img/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/store/types.tsx: -------------------------------------------------------------------------------- 1 | export interface StoreState { 2 | allExps: Exp[], 3 | expsOfType: Exp[], 4 | detailOfExp: Exp 5 | } -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 62.5% 3 | } 4 | html, body { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | a { 9 | text-decoration: none; 10 | color: black; 11 | } 12 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | const Url: string = 'https://www.holyzheng.top/api/public'; 2 | const SpecialPassCode = 'U3BlY2lhbFBhc3NDb2Rl'; 3 | 4 | export default { 5 | Url, 6 | SpecialPassCode, 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | width: 2.4rem; height: 2.4rem; 3 | font-size: 2.4rem; 4 | vertical-align: -0.15em; 5 | margin: 0.6rem 1rem 0.6rem 1.2rem; 6 | fill: currentColor; 7 | overflow: hidden; 8 | } -------------------------------------------------------------------------------- /src/store/store.tsx: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import { expReducer } from './reducers'; 3 | 4 | const store = createStore(expReducer, { 5 | allExps: [], 6 | expsOfType: [], 7 | detailOfExp: {} 8 | }) 9 | 10 | export default store; -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | // "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "extends": [], 4 | "defaultSeverity": "warning", 5 | "linterOptions": { 6 | "exclude": [ 7 | "config/**/*.js", 8 | "node_modules/**/*.ts", 9 | "coverage/lcov-report/*.js", 10 | "src/style/iconfont.js" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/store/constants.tsx: -------------------------------------------------------------------------------- 1 | export const EDIT_ALL_EXPS = 'EDIT_ALL_EXPS'; 2 | // 给类型定义别名 这里 EDIT_ALL_EXPS 与 string等价 3 | export type EDIT_ALL_EXPS = typeof EDIT_ALL_EXPS; 4 | 5 | export const EDIT_TYPE_EXPS = 'EDIT_TYPE_EXPS'; 6 | export type EDIT_TYPE_EXPS = typeof EDIT_TYPE_EXPS; 7 | 8 | export const EDIT_EXP_DETAIL = 'EDIT_EXP_DETAIL'; 9 | export type EDIT_EXP_DETAIL = typeof EDIT_EXP_DETAIL 10 | -------------------------------------------------------------------------------- /src/views/SpExp/SpExp.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/common.scss'; 2 | .sp-header { 3 | height: 4.2rem; 4 | text-align: center; 5 | background-color: $theme-color; 6 | font-size: 1.6rem; 7 | color: #fff; 8 | span { 9 | line-height: 4.2rem; 10 | } 11 | } 12 | .no-res { 13 | padding-top: 4.6rem; 14 | font-size: 1.4rem; 15 | line-height: 1.8rem; 16 | text-align: center; 17 | color: rgb(145, 145, 145); 18 | } -------------------------------------------------------------------------------- /src/components/Icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './Icon.scss'; 3 | 4 | interface Props { 5 | name: string 6 | } 7 | 8 | class Icon extends React.Component { 9 | constructor(props: Props) { 10 | super(props); 11 | } 12 | render() { 13 | return ( 14 | 17 | ) 18 | } 19 | } 20 | 21 | export default Icon 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "浏览我们的实验吧!(react版)", 3 | "short_name": "BrowseExp", 4 | "icons": [ 5 | { 6 | "src": "/img/icons/icon-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/img/icons/icon-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/index.html", 17 | "display": "standalone", 18 | "background_color": "#A4ACDB", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-router-dom'; 2 | declare module 'fastclick'; 3 | 4 | interface User { 5 | name: string; 6 | picture: string; 7 | user_id: number; 8 | } 9 | interface Exp { 10 | experiment_id: number; 11 | publisher_id: number; 12 | publisher_name: string; 13 | title: string; 14 | type: string; 15 | duration: string; 16 | pay: string; 17 | position: string; 18 | request: string; 19 | period: string; 20 | others: string; 21 | time: Date; 22 | application: string; 23 | content: string; 24 | user: User; 25 | } 26 | -------------------------------------------------------------------------------- /src/store/reducers.tsx: -------------------------------------------------------------------------------- 1 | import { ExpAction } from './actions'; 2 | import { StoreState } from './types'; 3 | import * as types from './constants'; 4 | 5 | export function expReducer(state: StoreState, action: ExpAction): StoreState { 6 | switch (action.type) { 7 | case types.EDIT_ALL_EXPS: 8 | return {...state, allExps: action.expArray} 9 | case types.EDIT_TYPE_EXPS: 10 | return {...state, expsOfType: action.expArray} 11 | case types.EDIT_EXP_DETAIL: 12 | return {...state, detailOfExp: action.detailOfExp} 13 | } 14 | return state; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Skeleton/SkeletonExp.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './SkeletonExp.scss'; 3 | 4 | class SkeletonExp extends React.Component<{},{}> { 5 | constructor(props: {}) { 6 | super(props); 7 | } 8 | render() { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ) 20 | } 21 | } 22 | 23 | export default SkeletonExp; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es6", "dom"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | // "rootDir": "src", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | // "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "build", 24 | "scripts", 25 | "acceptance-tests", 26 | "webpack", 27 | "jest", 28 | "src/setupTests.ts", 29 | "src/style/iconfont.js" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ExpBrief/ExpBrief.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/common.scss'; 2 | 3 | .exp-brief { 4 | font-size: 1.1rem; 5 | @include fja(flex-start); 6 | border-bottom: 0.1rem solid #F7F2F2; 7 | figure { 8 | width: 8rem; 9 | text-align: center; 10 | margin-left: 2rem; 11 | margin-right: 2rem; 12 | .portrait { 13 | margin-top: 1rem; 14 | @include circular(2rem); 15 | } 16 | p { 17 | @include text-deal; 18 | } 19 | } 20 | section { 21 | flex-grow: 1; 22 | position: relative; 23 | h3 + p { 24 | color: #797373; 25 | } 26 | p { 27 | @include text-deal; 28 | } 29 | big { 30 | color: #E83232; 31 | } 32 | .more { 33 | @include right-bottom(2rem); 34 | .arrow { 35 | width: 2rem; 36 | height: 3rem; 37 | @include to-rotate(-90deg) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom' 4 | import App from './App'; 5 | import './index.scss'; 6 | import 'normalize.css'; 7 | import './style/iconfont'; 8 | import FastClick from 'fastclick'; 9 | 10 | import * as serviceWorker from './serviceWorker'; 11 | 12 | // 移动端300ms延迟问题,同时解决点透事件的出现。 13 | if ('addEventListener' in document) { 14 | document.addEventListener('DOMContentLoaded', () => { 15 | FastClick.attach(document.body); 16 | }); 17 | } 18 | 19 | ReactDOM.render( 20 | 21 | 22 | , 23 | document.getElementById('root') 24 | ); 25 | 26 | // If you want your app to work offline and load faster, you can change 27 | // unregister() to register() below. Note this comes with some pitfalls. 28 | // Learn more about service workers: http://bit.ly/CRA-PWA 29 | serviceWorker.register(); 30 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | import { Route } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import store from './store/store'; 6 | 7 | import Home from './views/Home/Home'; 8 | import SelectType from './views/SelectType/SelectType'; 9 | import SpExp from './views/SpExp/SpExp'; 10 | import ExpDetailContainer from './views/ExpDetailContainer/ExpDetailContainer'; 11 | 12 | class App extends React.Component { 13 | public render() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/store/actions.tsx: -------------------------------------------------------------------------------- 1 | import * as constants from './constants'; 2 | 3 | export interface EditAllExpsArray { 4 | type: constants.EDIT_ALL_EXPS, 5 | expArray: Exp[] 6 | } 7 | 8 | export interface EditTypeExpsArray { 9 | type: constants.EDIT_TYPE_EXPS, 10 | expArray: Exp[] 11 | } 12 | 13 | export interface EditExpDetail { 14 | type: constants.EDIT_EXP_DETAIL, 15 | detailOfExp: Exp 16 | } 17 | 18 | export type ExpAction = EditAllExpsArray | EditTypeExpsArray | EditExpDetail; 19 | 20 | export function editAllExpsArray(expArray: Exp[]): EditAllExpsArray { 21 | return { 22 | type: constants.EDIT_ALL_EXPS, 23 | expArray 24 | } 25 | } 26 | 27 | export function editTypeExpsArray(expArray: Exp[]): EditTypeExpsArray { 28 | return { 29 | type: constants.EDIT_TYPE_EXPS, 30 | expArray 31 | } 32 | } 33 | 34 | export function editExpDetail(detailOfExp: Exp): EditExpDetail { 35 | return { 36 | type: constants.EDIT_EXP_DETAIL, 37 | detailOfExp 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/api/data.ts: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | import {AxiosPromise} from 'axios'; 3 | import config from '../config'; 4 | 5 | const baseUrl: string = config.Url; 6 | 7 | const axios = Axios.create({ 8 | method: 'get', 9 | baseURL: baseUrl, 10 | headers: { 11 | 'Authorization': `Bearer ${config.SpecialPassCode}`, 12 | 'withCredentials': true, 13 | }, 14 | }); 15 | 16 | export const getAllExperiments = (limit: number, offset: number): AxiosPromise => { 17 | return axios('/experiments', { 18 | params: { 19 | limit, 20 | offset, 21 | }, 22 | }); 23 | }; 24 | 25 | export const getExperimentsByType = (type: string, limit: number, offset: number): AxiosPromise => { 26 | const url = encodeURI('/experiment_types/' + type); 27 | return axios.get(url, { 28 | params: { 29 | limit, 30 | offset, 31 | }, 32 | }); 33 | }; 34 | 35 | export const getExperiment = (id: number): AxiosPromise => { 36 | const url = '/experiments/' + id; 37 | return axios.get(url); 38 | }; 39 | -------------------------------------------------------------------------------- /src/views/Home/Home.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/common.scss'; 2 | .home-header { 3 | position: sticky; 4 | top: 0; 5 | z-index: 9999; 6 | height: 5.4rem; 7 | padding-top: 1rem; 8 | background-color: $theme-color; 9 | text-align: center; 10 | font-size: 1.6rem; 11 | color: #fff; 12 | margin-bottom: 0; 13 | } 14 | .below-header { 15 | height: 4.6rem; 16 | background-color: $theme-color; 17 | @include shadow; 18 | } 19 | .home-nav { 20 | height: 10rem; 21 | @include fja(space-around); 22 | .to-type, .to-exp { 23 | width: 40%; 24 | height: 6rem; 25 | border-radius: 0.6rem; 26 | font-size: 1.8rem; 27 | @include fja(flex-start); 28 | @include shadow; 29 | } 30 | .to-type { 31 | background-color: #B7E8DF; 32 | } 33 | .to-exp { 34 | background-color: #A3ADE8 35 | } 36 | } 37 | .sec-title { 38 | @include fja(flex-start); 39 | strong { 40 | font-size: 1.6rem; 41 | margin-left: 2rem; 42 | } 43 | img { 44 | width: 4rem; 45 | height: 3rem; 46 | @include to-rotate(-90deg) 47 | } 48 | } -------------------------------------------------------------------------------- /src/views/SelectType/SelectType.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/common.scss'; 2 | .st-header { 3 | height: 4.2rem; 4 | text-align: center; 5 | background-color: $theme-color; 6 | font-size: 1.6rem; 7 | color: #fff; 8 | margin-bottom: 2rem; 9 | span { 10 | line-height: 4.2rem; 11 | } 12 | } 13 | .type-item { 14 | display: block; 15 | width: 90%; 16 | margin: 1.6rem auto; 17 | height: 5rem; 18 | line-height: 5rem; 19 | text-align: center; 20 | border-radius: 0.6rem; 21 | position: relative; 22 | font-size: 1.6rem; 23 | @include shadow; 24 | &.one { 25 | background-color: $exp-one; 26 | } 27 | &.two { 28 | background-color: $exp-two; 29 | } 30 | &.three { 31 | background-color: $exp-three; 32 | } 33 | &.four { 34 | background-color: $exp-four; 35 | } 36 | &.five { 37 | background-color: $exp-five; 38 | } 39 | &.other { 40 | border: 0.2rem solid #eee; 41 | } 42 | .icon { 43 | width: 2.2rem; 44 | @include left-top(0.6rem, 50%); 45 | transform: translateY(-80%); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 基于cra2与workbox开发的pwa 2 | typescript与react开发的预览实验的pwa,实践练手目的引入了react-redux, react-router。 3 | 4 | 详细描述文章: 5 | 6 | [记一次基于react、cra2、typescript的pwa项目由开发到部署(一)](https://juejin.im/post/5bd723c351882528d66cecd6) 7 | 8 | [记一次基于react、cra2、typescript的pwa项目由开发到部署(二)](https://juejin.im/post/5bd827716fb9a05d212ee743) 9 | 10 | [记一次基于react、cra2、typescript的pwa项目由开发到部署(三)](https://juejin.im/post/5c21a3f4e51d453634701952) 11 | 12 | 为什么做这个项目? 13 | 1. cra2开始支持pwa 14 | 2. 练手react与pwa 15 | 3. 搞事情 16 | 17 | 效果: 18 | 19 | ![bereactshow](https://raw.githubusercontent.com/HolyZheng/holyZheng-blog/master/images/browse-exp-react.gif) 20 | 21 | ![addToScreen](https://raw.githubusercontent.com/HolyZheng/holyZheng-blog/master/images/addToScreen.png) 22 | 23 | 二维码体验项目: 24 | > note: 25 | > 1. 建议用uc浏览器打开,因为uc浏览器对pwa的支持较好。 26 | > 2. "添加到桌面的提示" 需要短时间多次进入web app 才会触发 27 | 28 | ![qrcode](https://user-gold-cdn.xitu.io/2018/10/29/166c062f68fcec88?w=299&h=292&f=png&s=14209) 29 | 30 | 项目地址:[browseExpByReact](https://github.com/HolyZheng/browseExpbyReact) 31 | 32 | 如果感兴趣,可以对比着基于vue的实现来看: [browseExpByVue](https://github.com/HolyZheng/BrowseExp) 33 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | /* config-overrides.js */ 2 | 3 | const rewireTypescript = require('react-app-rewire-typescript'); 4 | const workboxPlugin = require('workbox-webpack-plugin') 5 | const path = require('path') 6 | 7 | module.exports = { 8 | webpack: function (config, env) { 9 | config = rewireTypescript(config, env); 10 | if (env === 'production') { 11 | const workboxConfigProd = { 12 | swSrc: path.join(__dirname, 'public', 'cus-service-worker.js'), 13 | swDest: 'cus-service-worker.js', 14 | importWorkboxFrom: 'disabled' 15 | } 16 | config = removePreWorkboxWebpackPluginConfig(config) 17 | config.plugins.push(new workboxPlugin.InjectManifest(workboxConfigProd)) 18 | } 19 | return config 20 | } 21 | } 22 | 23 | function removePreWorkboxWebpackPluginConfig (config) { 24 | const preWorkboxPluginIndex = config.plugins.findIndex((element) => { 25 | return Object.getPrototypeOf(element).constructor.name === 'GenerateSW' 26 | }) 27 | if (preWorkboxPluginIndex !== -1) { 28 | config.plugins.splice(preWorkboxPluginIndex, 1) 29 | } 30 | return config 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react-redux": "^6.0.9", 7 | "axios": "^0.18.0", 8 | "fastclick": "^1.0.6", 9 | "node-sass": "^4.9.4", 10 | "normalize.css": "^8.0.0", 11 | "react": "^16.6.0", 12 | "react-dom": "^16.6.0", 13 | "react-redux": "^5.1.0", 14 | "react-router-dom": "^4.3.1", 15 | "react-scripts": "2.0.5", 16 | "redux": "^4.0.1" 17 | }, 18 | "scripts": { 19 | "start": "react-app-rewired start", 20 | "build": "react-app-rewired build", 21 | "test": "react-app-rewired test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": [ 28 | ">0.2%", 29 | "not dead", 30 | "not ie <= 11", 31 | "not op_mini all" 32 | ], 33 | "devDependencies": { 34 | "path": "^0.12.7", 35 | "react-app-rewire-typescript": "^2.0.2", 36 | "react-app-rewired": "^1.6.2", 37 | "ts-loader": "^5.2.2", 38 | "typescript": "^3.1.3", 39 | "workbox-webpack-plugin": "^3.6.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/cus-service-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js'); 2 | 3 | if (workbox) { 4 | console.log(`Yay! Workbox is loaded 🎉`); 5 | } else { 6 | console.log(`Boo! Workbox didn't load 😬`); 7 | } 8 | // set the prefix and suffix of our sw's name 9 | workbox.core.setCacheNameDetails({ 10 | prefix: 'browse-exp', 11 | suffix: 'v1.0.0', 12 | }); 13 | // have our sw update and control a web page as soon as possible. 14 | workbox.skipWaiting(); 15 | workbox.clientsClaim(); 16 | 17 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 18 | workbox.precaching.suppressWarnings(); 19 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 20 | 21 | 22 | // cache our data, and use networkFirst strategy. 23 | workbox.routing.registerRoute( 24 | new RegExp('.*experiments\?.*'), 25 | workbox.strategies.networkFirst() 26 | ); 27 | workbox.routing.registerRoute( 28 | new RegExp('.*experiments/\\d'), 29 | workbox.strategies.networkFirst() 30 | ) 31 | workbox.routing.registerRoute( 32 | new RegExp('.*experiment_types.*'), 33 | workbox.strategies.networkFirst() 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /src/components/ExpBrief/ExpBrief.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import './ExpBrief.scss'; 4 | 5 | import down from '../../images/down.png'; 6 | 7 | interface Props { 8 | picture: string, 9 | name: string, 10 | title: string, 11 | type: string, 12 | pay: string, 13 | duration: string, 14 | id: number 15 | } 16 | 17 | class ExpBrief extends React.Component { 18 | constructor(props: Props) { 19 | super(props); 20 | } 21 | render() { 22 | return ( 23 | 24 |
25 | 26 |

{this.props.name}

27 |
28 |
29 |

{this.props.title}

30 |

分类:{this.props.type}

31 |

薪酬:¥{this.props.pay} / {this.props.duration}min

32 |
33 | 详情 34 | 35 |
36 |
37 | 38 | ) 39 | } 40 | } 41 | 42 | export default ExpBrief; 43 | -------------------------------------------------------------------------------- /src/components/ExpDetail/ExpDetail.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/common.scss'; 2 | .exp-detail-header { 3 | height: 6rem; 4 | border-bottom: 0.1rem solid #eee; 5 | @include shadow; 6 | img { 7 | @include circular(2rem); 8 | vertical-align: middle; 9 | margin: 1rem 1rem 2.4rem 1rem; 10 | } 11 | p { 12 | display: inline-block; 13 | margin: 0; 14 | font-size: 1.8rem; 15 | width: 12rem; 16 | @include text-deal; 17 | } 18 | } 19 | .exp-detail-section { 20 | box-sizing: border-box; 21 | padding: 1rem 1.6rem; 22 | p { 23 | font-size: 1.6rem; 24 | margin: 0.4rem 0; 25 | &.segment { 26 | padding-bottom: 0.6rem; 27 | border-bottom: 0.1rem dashed #eeeeee; 28 | } 29 | small { 30 | display: inline-block; 31 | line-height: 1.6rem; 32 | margin: 0.6rem 1rem 0.6rem 1rem; 33 | } 34 | } 35 | } 36 | .exp-detail-footer { 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | button { 41 | width: 90%; 42 | height: 4rem; 43 | border-radius: 0.6rem; 44 | margin-top: 1rem; 45 | font-size: 1.6rem; 46 | color: #fff; 47 | border-style: none; 48 | background-color: $green; 49 | } 50 | p { 51 | text-align: center; 52 | font-size: 1rem; 53 | color: rgb(194, 193, 193); 54 | } 55 | } -------------------------------------------------------------------------------- /src/style/common.scss: -------------------------------------------------------------------------------- 1 | $theme-color: #6476DB; 2 | $green: #8BC34A; 3 | $red: #EA7382; 4 | $exp-one: #EA7382; 5 | $exp-two: #C4E3A0; 6 | $exp-three: #A4ACDB; 7 | $exp-four: #FBDBAB; 8 | $exp-five: #BBBBBB; 9 | 10 | // 将块级元素设置为半径为 radius的圆形 11 | @mixin circular($radius) { 12 | width: $radius*2; 13 | height: $radius*2; 14 | -webkit-border-radius: $radius; 15 | -moz-border-radius: $radius; 16 | -ms-border-radius: $radius; 17 | -o-border-radius: $radius; 18 | border-radius: $radius; 19 | } 20 | 21 | // flex布局 22 | @mixin fja($j: space-between, $a: center) { 23 | display: flex; 24 | justify-content: $j; 25 | align-items: $a 26 | } 27 | 28 | // 旋转元素 29 | @mixin to-rotate($d) { 30 | transform:rotate($d); 31 | -ms-transform:rotate($d); 32 | -moz-transform:rotate($d); 33 | -webkit-transform:rotate($d); 34 | -o-transform:rotate($d); 35 | } 36 | 37 | // 绝对定位元素 右下角 38 | @mixin right-bottom($r: 0, $b: 0) { 39 | position: absolute; 40 | bottom: $b; 41 | right: $r; 42 | } 43 | 44 | @mixin left-top($l: 0, $t: 0) { 45 | position: absolute; 46 | left: $l; 47 | top: $t; 48 | } 49 | 50 | // 处理文字,包括英语,超过长度显示省略号 51 | @mixin text-deal { 52 | white-space: nowrap; 53 | overflow: hidden; 54 | text-overflow: ellipsis; 55 | } 56 | 57 | // 同一标准的阴影 58 | @mixin shadow { 59 | box-shadow: 0 0 0.8rem rgb(196, 194, 194) 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/views/SelectType/SelectType.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import './SelectType.scss'; 4 | 5 | import Icon from '../../components/Icon/Icon'; 6 | 7 | class SelectType extends React.Component<{}, {}> { 8 | constructor(props: {}) { 9 | super(props); 10 | } 11 | render() { 12 | return ( 13 | 14 |
15 | 实验分类 16 |
17 | 18 | 行为实验 19 | 20 | 21 | 脑电实验 22 | 23 | 24 | 问卷 25 | 26 | 27 | 核磁共振 28 | 29 | 30 | 皮肤电实验 31 | 32 | 33 | 其他 34 | 35 |
36 | ) 37 | } 38 | } 39 | 40 | export default SelectType; 41 | -------------------------------------------------------------------------------- /src/views/ExpDetailContainer/ExpDetailContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as actions from '../../store/actions'; 3 | import { StoreState } from '../../store/types'; 4 | import { connect } from 'react-redux'; 5 | import { getExperiment } from '../../api/data'; 6 | 7 | import ExpDetail from '../../components/ExpDetail/ExpDetail' 8 | 9 | interface Props { 10 | match: any, 11 | setExp: (exp: Exp) => void, 12 | exp: Exp 13 | } 14 | 15 | class ExpDetailContainer extends React.Component { 16 | constructor(props: Props) { 17 | super(props); 18 | } 19 | componentWillMount() { 20 | const ctx = this; 21 | const id: number = ctx.props.match.params.id; 22 | getExperiment(id) 23 | .then((res:any) => { 24 | ctx.props.setExp(res.data) 25 | }) 26 | .catch((err: any) => { 27 | throw new Error(err); 28 | }); 29 | } 30 | render() { 31 | const ctx = this; 32 | return ( 33 | 34 | ) 35 | } 36 | } 37 | 38 | export function mapStateToProps(state: StoreState) { 39 | return { 40 | exp: state.detailOfExp 41 | } 42 | } 43 | 44 | export function mapDispatchToProps(dispatch: Function) { 45 | return { 46 | setExp: (exp: Exp) => {dispatch(actions.editExpDetail(exp))} 47 | } 48 | } 49 | 50 | export default connect(mapStateToProps, mapDispatchToProps)(ExpDetailContainer); 51 | -------------------------------------------------------------------------------- /src/components/ExpDetail/ExpDetail.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './ExpDetail.scss'; 3 | 4 | interface Props { 5 | exp: Exp 6 | } 7 | 8 | class ExpDetail extends React.Component { 9 | constructor(props: Props) { 10 | super(props) 11 | } 12 | render() { 13 | const exp = this.props.exp; 14 | return ( 15 | 16 |
17 | 18 |

{exp.user && exp.user.name}

19 |
20 |
21 |

实验类型:{exp.type}

22 |

实验时长:{exp.duration} min

23 |

薪酬:¥{exp.pay}

24 |

实验地点:
25 | {exp.position} 26 |

27 |

被试要求:
28 | {exp.request} 29 |

30 |

可选时间段:
31 | {exp.period} 32 |

33 |

实验内容:
34 | {exp.content} 35 |

36 |

其他信息:
37 | {exp.others} 38 |

39 |

报名方式:
40 | {exp.application} 41 |

42 |
43 |
44 | ) 45 | } 46 | } 47 | 48 | export default ExpDetail; 49 | -------------------------------------------------------------------------------- /src/components/Skeleton/SkeletonExp.scss: -------------------------------------------------------------------------------- 1 | @keyframes placeHolderShimmer{ 2 | 0%{ 3 | background-position: 50% 0 4 | } 5 | 100%{ 6 | background-position: -50% 0 7 | } 8 | } 9 | .skeleton-wrap { 10 | box-sizing: border-box; 11 | height: 10rem; 12 | background-color: #fff; 13 | padding: 2rem; 14 | .skeleton { 15 | height: 6rem; 16 | box-sizing: border-box; 17 | position: relative; 18 | animation-duration: 1s; 19 | animation-fill-mode: forwards; 20 | animation-iteration-count: infinite; 21 | animation-name: placeHolderShimmer; 22 | animation-timing-function: linear; 23 | background: linear-gradient(to right, #eeeeee, #dddddd 10%, #eeeeee 30%); 24 | background-size: 400% 400%; 25 | .item { 26 | position: absolute; 27 | } 28 | .left-right { 29 | height: 6rem; 30 | width: 2rem; 31 | top: 0; 32 | left: 6rem; 33 | background-color: #fff; 34 | } 35 | .line-zero { 36 | height: 0.4rem; 37 | width: calc(100% - 8rem); 38 | background-color: #fff; 39 | left: 8rem; 40 | top: 0; 41 | } 42 | .line-one { 43 | height: 1rem; 44 | width: calc(100% - 8rem); 45 | background-color: #fff; 46 | left: 8rem; 47 | top: 2rem; 48 | } 49 | .line-two { 50 | height: 1rem; 51 | width: 6rem; 52 | background-color: #fff; 53 | top: 3rem; 54 | right: 0rem; 55 | } 56 | .line-three { 57 | width: calc(100% - 8rem); 58 | height: 2rem; 59 | background-color: #fff; 60 | bottom: 0; 61 | right: 0; 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import * as actions from '../../store/actions'; 4 | import { StoreState } from '../../store/types'; 5 | import { connect } from 'react-redux'; 6 | import { getAllExperiments } from '../../api/data' 7 | import './Home.scss'; 8 | 9 | import down from '../../images/down.png'; 10 | import Icon from '../../components/Icon/Icon'; 11 | import SkeletonExp from '../../components/Skeleton/SkeletonExp'; 12 | import ExpBrief from '../../components/ExpBrief/ExpBrief'; 13 | 14 | interface State { 15 | complete: boolean 16 | } 17 | interface Props { 18 | expArray: Exp[], 19 | setAllExps: (expArray: Exp[]) => void 20 | } 21 | 22 | class Home extends React.Component { 23 | constructor(props: Props) { 24 | super(props); 25 | this.state = { 26 | complete: false 27 | }; 28 | } 29 | 30 | componentWillMount() { 31 | const ctx = this; 32 | getAllExperiments(5, 0) 33 | .then((res: any) => { 34 | ctx.props.setAllExps(res.data) 35 | ctx.setState({ 36 | complete: true 37 | }) 38 | }) 39 | .catch((err: any) => { 40 | ctx.setState({ 41 | complete: false 42 | }) 43 | throw new Error(err); 44 | }) 45 | } 46 | 47 | render() { 48 | 49 | let viewList; 50 | if (this.state.complete) { 51 | viewList = this.props.expArray.map((item: Exp) => 52 |
53 | 54 |
55 | ); 56 | } else { 57 | viewList = ; 58 | } 59 | 60 | return ( 61 | 62 |
63 | 实验预览版本
64 | The version for Browse(react) 65 |
66 |
67 | 77 |
78 |
79 | 实验广场 80 | 81 |
82 | {viewList} 83 |
84 |
85 | ) 86 | } 87 | } 88 | 89 | export function mapStateToProps({allExps}: StoreState) { 90 | return { 91 | expArray: allExps 92 | } 93 | } 94 | 95 | export function mapDispatchToProps(dispatch: Function) { 96 | return { 97 | setAllExps: (expArray: Exp[]) => {dispatch(actions.editAllExpsArray(expArray))} 98 | } 99 | } 100 | 101 | export default connect(mapStateToProps, mapDispatchToProps)(Home); 102 | -------------------------------------------------------------------------------- /src/views/SpExp/SpExp.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './SpExp.scss'; 3 | import { getExperimentsByType, getAllExperiments } from '../../api/data'; 4 | import { StoreState } from '../../store/types'; 5 | import { connect } from 'react-redux'; 6 | import * as actions from '../../store/actions'; 7 | 8 | import ExpBrief from '../../components/ExpBrief/ExpBrief'; 9 | import SkeletonExp from '../../components/Skeleton/SkeletonExp'; 10 | 11 | interface State { 12 | complete: boolean 13 | } 14 | 15 | interface Props { 16 | expArray: Exp[], 17 | match: any, 18 | setSpExps: (expArray: Exp[]) => void 19 | } 20 | 21 | class SpExp extends React.Component { 22 | constructor(props: Props) { 23 | super(props); 24 | this.state = { 25 | complete: false 26 | } 27 | } 28 | 29 | componentWillMount() { 30 | const ctx = this; 31 | const params = this.props.match.params; 32 | if (params.type !== '实验广场') { 33 | getExperimentsByType(params.type, 5, 0) 34 | .then((res: any) => { 35 | ctx.setState({ 36 | complete: true, 37 | }) 38 | ctx.props.setSpExps(res.data) 39 | }) 40 | .catch((err: any) => { 41 | ctx.setState({ 42 | complete: false 43 | }) 44 | throw new Error(err); 45 | }) 46 | } else { 47 | getAllExperiments(5, 0) 48 | .then((res: any) => { 49 | ctx.setState({ 50 | complete: true, 51 | }) 52 | ctx.props.setSpExps(res.data) 53 | }) 54 | .catch((err: any) => { 55 | ctx.setState({ 56 | complete: false 57 | }) 58 | throw new Error(err); 59 | }) 60 | } 61 | } 62 | 63 | render() { 64 | let viewList; 65 | if (this.state.complete) { 66 | if (this.props.expArray.length) { 67 | viewList = this.props.expArray.map((item: Exp) => 68 |
69 | 71 |
72 | ); 73 | } else { 74 | viewList = ( 75 |
76 | 暂时没有相关实验呢,
稍后再查看吧~ 77 |
78 | ) 79 | } 80 | } else { 81 | viewList = ; 82 | } 83 | return ( 84 | 85 |
86 | {this.props.match.params.type} 87 |
88 |
89 | {viewList} 90 |
91 |
92 | ) 93 | } 94 | } 95 | 96 | export function mapStateToProps({expsOfType}: StoreState) { 97 | return { 98 | expArray: expsOfType 99 | } 100 | } 101 | 102 | export function mapDispatchToProps(dispatch: Function) { 103 | return { 104 | setSpExps: (expsOfType: Exp[]) => {dispatch(actions.editTypeExpsArray(expsOfType))} 105 | } 106 | } 107 | 108 | export default connect(mapStateToProps, mapDispatchToProps)(SpExp); 109 | -------------------------------------------------------------------------------- /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 http://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); 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}/cus-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 http://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 | installingWorker.onstatechange = () => { 64 | if (installingWorker.state === 'installed') { 65 | if (navigator.serviceWorker.controller) { 66 | // At this point, the updated precached content has been fetched, 67 | // but the previous service worker will still serve the older 68 | // content until all client tabs are closed. 69 | console.log( 70 | 'New content is available and will be used when all ' + 71 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 72 | ); 73 | 74 | // Execute callback 75 | if (config && config.onUpdate) { 76 | config.onUpdate(registration); 77 | } 78 | } else { 79 | // At this point, everything has been precached. 80 | // It's the perfect time to display a 81 | // "Content is cached for offline use." message. 82 | console.log('Content is cached for offline use.'); 83 | 84 | // Execute callback 85 | if (config && config.onSuccess) { 86 | config.onSuccess(registration); 87 | } 88 | } 89 | } 90 | }; 91 | }; 92 | }) 93 | .catch(error => { 94 | console.error('Error during service worker registration:', error); 95 | }); 96 | } 97 | 98 | function checkValidServiceWorker(swUrl, config) { 99 | // Check if the service worker can be found. If it can't reload the page. 100 | fetch(swUrl) 101 | .then(response => { 102 | // Ensure service worker exists, and that we really are getting a JS file. 103 | if ( 104 | response.status === 404 || 105 | response.headers.get('content-type').indexOf('javascript') === -1 106 | ) { 107 | // No service worker found. Probably a different app. Reload the page. 108 | navigator.serviceWorker.ready.then(registration => { 109 | registration.unregister().then(() => { 110 | window.location.reload(); 111 | }); 112 | }); 113 | } else { 114 | // Service worker found. Proceed as normal. 115 | registerValidSW(swUrl, config); 116 | } 117 | }) 118 | .catch(() => { 119 | console.log( 120 | 'No internet connection found. App is running in offline mode.' 121 | ); 122 | }); 123 | } 124 | 125 | export function unregister() { 126 | if ('serviceWorker' in navigator) { 127 | navigator.serviceWorker.ready.then(registration => { 128 | registration.unregister(); 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/style/iconfont.js: -------------------------------------------------------------------------------- 1 | (function(window){var svgSprite='';var script=function(){var scripts=document.getElementsByTagName("script");return scripts[scripts.length-1]}();var shouldInjectCss=script.getAttribute("data-injectcss");var ready=function(fn){if(document.addEventListener){if(~["complete","loaded","interactive"].indexOf(document.readyState)){setTimeout(fn,0)}else{var loadFn=function(){document.removeEventListener("DOMContentLoaded",loadFn,false);fn()};document.addEventListener("DOMContentLoaded",loadFn,false)}}else if(document.attachEvent){IEContentLoaded(window,fn)}function IEContentLoaded(w,fn){var d=w.document,done=false,init=function(){if(!done){done=true;fn()}};var polling=function(){try{d.documentElement.doScroll("left")}catch(e){setTimeout(polling,50);return}init()};polling();d.onreadystatechange=function(){if(d.readyState=="complete"){d.onreadystatechange=null;init()}}}};var before=function(el,target){target.parentNode.insertBefore(el,target)};var prepend=function(el,target){if(target.firstChild){before(el,target.firstChild)}else{target.appendChild(el)}};function appendSvg(){var div,svg;div=document.createElement("div");div.innerHTML=svgSprite;svgSprite=null;svg=div.getElementsByTagName("svg")[0];if(svg){svg.setAttribute("aria-hidden","true");svg.style.position="absolute";svg.style.width=0;svg.style.height=0;svg.style.overflow="hidden";prepend(svg,document.body)}}if(shouldInjectCss&&!window.__iconfont__svg__cssinject__){window.__iconfont__svg__cssinject__=true;try{document.write("")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window) --------------------------------------------------------------------------------