Browse Source

项目搭建

dev01 1 year ago
commit
f36cddc920
84 changed files with 16306 additions and 0 deletions
  1. 12 0
      .editorconfig
  2. 12 0
      .eslintrc
  3. 5 0
      .gitignore
  4. 24 0
      CHANGELOG.md
  5. 21 0
      LICENSE
  6. 54 0
      README.md
  7. 36 0
      config/dev.js
  8. 152 0
      config/index.js
  9. 22 0
      config/prod.js
  10. 71 0
      package.json
  11. 16 0
      project.config.json
  12. 16 0
      src/actions/increase.js
  13. 16 0
      src/actions/list.js
  14. 15 0
      src/actions/user.js
  15. 78 0
      src/app.js
  16. 1 0
      src/app.scss
  17. BIN
      src/assets/default-avatar.png
  18. BIN
      src/assets/logo.acd490fa.png
  19. BIN
      src/assets/tab-bar/cart-active.png
  20. BIN
      src/assets/tab-bar/cart.png
  21. BIN
      src/assets/tab-bar/cate-active.png
  22. BIN
      src/assets/tab-bar/cate.png
  23. BIN
      src/assets/tab-bar/home-active.png
  24. BIN
      src/assets/tab-bar/home.png
  25. BIN
      src/assets/tab-bar/user-active.png
  26. BIN
      src/assets/tab-bar/user.png
  27. 54 0
      src/components/button/index.js
  28. 43 0
      src/components/button/index.scss
  29. BIN
      src/components/checkbox/assets/checked.png
  30. BIN
      src/components/checkbox/assets/un-checked.png
  31. 30 0
      src/components/checkbox/index.js
  32. 13 0
      src/components/checkbox/index.scss
  33. BIN
      src/components/home-title/aseets/arrow-right.png
  34. 30 0
      src/components/home-title/index.js
  35. 36 0
      src/components/home-title/index.scss
  36. 9 0
      src/components/index.js
  37. BIN
      src/components/input-item/assets/clear.png
  38. 59 0
      src/components/input-item/index.js
  39. 37 0
      src/components/input-item/index.scss
  40. BIN
      src/components/input-number/assets/minus-disabled.png
  41. BIN
      src/components/input-number/assets/minus.png
  42. BIN
      src/components/input-number/assets/plus.png
  43. 63 0
      src/components/input-number/index.js
  44. 52 0
      src/components/input-number/index.scss
  45. 57 0
      src/components/item-list/index.js
  46. 70 0
      src/components/item-list/index.scss
  47. BIN
      src/components/loading/assets/loading.gif
  48. 15 0
      src/components/loading/index.js
  49. 14 0
      src/components/loading/index.scss
  50. BIN
      src/components/popup/assets/close.png
  51. 66 0
      src/components/popup/index.js
  52. 51 0
      src/components/popup/index.scss
  53. 37 0
      src/components/tag/index.js
  54. 17 0
      src/components/tag/index.scss
  55. 16 0
      src/constants/api.js
  56. 1 0
      src/constants/increase.js
  57. 1 0
      src/constants/list.js
  58. 3 0
      src/constants/user.js
  59. 19 0
      src/index.html
  60. 58 0
      src/pages/increase/increase.js
  61. 0 0
      src/pages/increase/increase.scss
  62. 43 0
      src/pages/list/list.js
  63. 0 0
      src/pages/list/list.scss
  64. 94 0
      src/pages/user-login/user-login.js
  65. 54 0
      src/pages/user-login/user-login.scss
  66. 43 0
      src/pages/user/user.js
  67. 0 0
      src/pages/user/user.scss
  68. 17 0
      src/pages/webview/rn/index.js
  69. 41 0
      src/pages/webview/webview.js
  70. 3 0
      src/pages/webview/webview.scss
  71. 0 0
      src/reducers/increase.js
  72. 10 0
      src/reducers/index.js
  73. 47 0
      src/reducers/list.js
  74. 29 0
      src/reducers/user.js
  75. 28 0
      src/store/index.js
  76. 60 0
      src/styles/mixins.scss
  77. 47 0
      src/styles/reset.scss
  78. 16 0
      src/styles/theme.scss
  79. 38 0
      src/utils/jump.js
  80. 15 0
      src/utils/redux.js
  81. 83 0
      src/utils/request.js
  82. 48 0
      src/utils/style.js
  83. 28 0
      src/utils/taro.alipay.js
  84. 14160 0
      yarn.lock

+ 12 - 0
.editorconfig

@@ -0,0 +1,12 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false

+ 12 - 0
.eslintrc

@@ -0,0 +1,12 @@
+{
+  "extends": ["taro"],
+  "rules": {
+    "no-unused-vars": ["error", { "varsIgnorePattern": "Taro" }],
+    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }]
+  },
+  "parser": "babel-eslint",
+  "globals": {
+    "wx": false,
+    "my": false
+  }
+}

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+dist/
+.temp/
+rn_temp/
+node_modules/
+.DS_Store

+ 24 - 0
CHANGELOG.md

@@ -0,0 +1,24 @@
+# CHANGELOG
+
+## 1.3.0 (2019-07-22)
+
+* 升级 Taro 至 v1.3.4
+
+## 1.2.1 (2019-03-27)
+
+* 由于严选小程序首页界面、接口大幅变动,导致原先页面报错,暂时去掉不适配的部分
+
+## 1.2.0 (2019-03-13)
+
+* 升级 Taro 至 v1.2.17(@tarojs/components 有 bug,暂时用 v1.2.16)
+* 引入统一接口的多端文件方式处理多端兼容问题 (./src/user-login/user-login)
+
+## 1.1.0 (2019-03-05)
+
+* 升级 Taro 至 v1.2.15
+* 去掉 RN 绑定 onClick 时的样式兼容处理 [issue](https://github.com/NervJS/taro/issues/2205)
+* 修改根据不同环境引入不同组件的写法 (./src/pages/webview)
+
+## 1.0.0 (2019-02-20)
+
+* 正式开源

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 54 - 0
README.md

@@ -0,0 +1,54 @@
+# taro-yanxuan
+
+![overview](https://qit-team.github.io/public/images/taro-yanxuan/overview.png)
+
+首个 [Taro](https://github.com/NervJS/taro) 多端统一实例 - 网易严选(小程序 + H5 + React Native)。
+
+本项目基于**趣店 FED** 在 Taro 多端统一开发方面的实践经验,以网易严选小程序为载体,旨在探讨如何以正确的方式使用 Taro 进行多端开发,具体可查看文章 [Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)](https://juejin.im/post/5c6a151f518825625e4ac830)。
+
+## 效果预览
+
+小程序端已支持微信小程序、支付宝小程序,但无法提供在线版,请 clone 代码本地运行。
+
+本项目直接调用的网易严选接口,若要体验登录、购物车功能,请使用网易邮箱账号登录。
+
+| 小程序 | H5 - [访问链接](http://jsnewbee.com/taro-yanxuan/) | React Native |
+| :--------: | :--------:| :--------: |
+| 请 clone 代码本地运行 | ![H5](https://qit-team.github.io/public/images/taro-yanxuan/h5-qr-code.png) | [Expo Snacks](https://snack.expo.io/@caiminxing/taro-yanxuan) |
+
+如下是 **React Native** 的运行截图:
+
+| 首页、分类 | 详情、加购物车 | 购物车、个人 |
+| :--------: | :--------:| :--------: |
+| ![首页、分类](https://qit-team.github.io/public/images/taro-yanxuan/video-01.gif) | ![二级分类、详情](https://qit-team.github.io/public/images/taro-yanxuan/video-02.gif) | ![购物车、个人](https://qit-team.github.io/public/images/taro-yanxuan/video-03.gif) |
+
+## 本地运行
+
+本项目直接调用的网易严选接口(网易登录接口改动,暂时无法登陆)。
+
+``` bash
+# 安装依赖,或 npm i
+yarn
+
+# 运行小程序,编译后的文件位于项目下的 dist 文件夹
+# (微信 dev:weapp,支付宝 dev:alipay)
+npm run dev:weapp
+
+# 运行 H5
+npm run dev:h5
+
+# 运行 React Native,请务必查阅文档:https://nervjs.github.io/taro/docs/react-native.html
+npm run dev:rn
+```
+
+## 项目说明
+
+本项目的主要目的是阐述多端开发思路、技巧,力求直观、方便阅读,因此是在 `taro init` 的基础上开发的,没有去做太多封装。诸如是用 Redux 还是 Mobx,网络请求怎么封装比较完美并不是本项目的重点。
+
+本项目不会去实现一个完整的网易严选,但也实现了足够多的功能、细节进行踩坑,具体涉及的踩坑点、注意事项都在代码中以注释 `// TODO`(Taro 还未支持的)、`// NOTE`(开发技巧、注意事项)注明了。
+
+最后,有一点很遗憾,就是一开始没有保留一份 mock 数据,后面网易严选改动挺大,主要是登录逻辑也变了,导致不少功能现在没法运行,后续有空会完善 mock 功能。
+
+## License
+
+MIT

+ 36 - 0
config/dev.js

@@ -0,0 +1,36 @@
+// NOTE H5 端使用 devServer 实现跨域,需要修改 package.json 的运行命令,加入环境变量
+const isH5 = process.env.CLIENT_ENV === 'h5'
+const HOST = '"https://miniapp.you.163.com"'
+const HOST_M = '"https://m.you.163.com"'
+
+module.exports = {
+  env: {
+    NODE_ENV: '"development"'
+  },
+  defineConstants: {
+    // HOST: isH5 ? '"/api"' : HOST,
+    HOST: isH5 ? '"http://140.210.193.73"' : HOST,
+    HOST_M: isH5 ? '"/api-m"' : HOST_M
+  },
+  weapp: {},
+  h5: {
+    devServer: {
+      proxy: {
+        '/api/': {
+          target: JSON.parse(HOST),
+          pathRewrite: {
+            '^/api/': '/'
+          },
+          changeOrigin: true
+        },
+        '/api-m/': {
+          target: JSON.parse(HOST_M),
+          pathRewrite: {
+            '^/api-m/': '/'
+          },
+          changeOrigin: true
+        }
+      }
+    }
+  }
+}

+ 152 - 0
config/index.js

@@ -0,0 +1,152 @@
+const path = require('path')
+
+// NOTE 在 sass 中通过别名(@ 或 ~)引用需要指定路径
+const sassImporter = function(url) {
+  if (url[0] === '~' && url[1] !== '/') {
+    return {
+      file: path.resolve(__dirname, '..', 'node_modules', url.substr(1))
+    }
+  }
+
+  const reg = /^@styles\/(.*)/
+  return {
+    file: reg.test(url) ? path.resolve(__dirname, '..', 'src/styles', url.match(reg)[1]) : url
+  }
+}
+
+const config = {
+  projectName: 'taro-yanxuan',
+  date: '2019-2-1',
+  designWidth: 750,
+  deviceRatio: {
+    '640': 2.34 / 2,
+    '750': 1,
+    '828': 1.81 / 2
+  },
+  sourceRoot: 'src',
+  outputRoot: 'dist',
+  plugins: {
+    babel: {
+      sourceMap: true,
+      presets: [
+        ['env', {
+          modules: false
+        }]
+      ],
+      plugins: [
+        'transform-decorators-legacy',
+        'transform-class-properties',
+        'transform-object-rest-spread'
+      ]
+    },
+    sass: {
+      importer: sassImporter
+    }
+  },
+  defineConstants: {
+  },
+  alias: {
+    '@actions': path.resolve(__dirname, '..', 'src/actions'),
+    '@assets': path.resolve(__dirname, '..', 'src/assets'),
+    '@components': path.resolve(__dirname, '..', 'src/components'),
+    '@constants': path.resolve(__dirname, '..', 'src/constants'),
+    '@reducers': path.resolve(__dirname, '..', 'src/reducers'),
+    '@styles': path.resolve(__dirname, '..', 'src/styles'),
+    '@utils': path.resolve(__dirname, '..', 'src/utils')
+  },
+  copy: {
+    patterns: [
+    ],
+    options: {
+    }
+  },
+  weapp: {
+    module: {
+      postcss: {
+        autoprefixer: {
+          enable: true,
+          config: {
+            browsers: [
+              'last 3 versions',
+              'Android >= 4.1',
+              'ios >= 8'
+            ]
+          }
+        },
+        pxtransform: {
+          enable: true,
+          config: {
+
+          }
+        },
+        url: {
+          enable: true,
+          config: {
+            limit: 10240 // 设定转换尺寸上限
+          }
+        },
+        cssModules: {
+          enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+          config: {
+            namingPattern: 'module', // 转换模式,取值为 global/module
+            generateScopedName: '[name]__[local]___[hash:base64:5]'
+          }
+        }
+      }
+    }
+  },
+  h5: {
+    // NOTE H5 打包静态资源时带 hash 值,方便缓存、版本管理
+    publicPath: '/',
+    staticDirectory: 'static',
+    output: {
+      filename: 'js/[name].[hash].js',
+      chunkFilename: 'js/[name].[chunkhash].js'
+    },
+    imageUrlLoaderOption: {
+      limit: 5000,
+      name: 'static/images/[name].[hash].[ext]'
+    },
+    miniCssExtractPluginOption: {
+      filename: 'css/[name].[hash].css',
+      chunkFilename: 'css/[name].[chunkhash].css',
+    },
+    module: {
+      postcss: {
+        autoprefixer: {
+          enable: true,
+          config: {
+            browsers: [
+              'last 3 versions',
+              'Android >= 4.1',
+              'ios >= 8'
+            ]
+          }
+        },
+        cssModules: {
+          enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+          config: {
+            namingPattern: 'module', // 转换模式,取值为 global/module
+            generateScopedName: '[name]__[local]___[hash:base64:5]'
+          }
+        }
+      }
+    },
+    sassLoaderOption: {
+      importer: sassImporter
+    }
+  },
+  rn: {
+    appJson: {
+      // NOTE taro-native-shell 中默认用的是 taroDemo
+      name: 'taroDemo'
+    }
+  }
+}
+
+module.exports = function (merge) {
+  if (process.env.NODE_ENV === 'development') {
+    return merge({}, config, require('./dev'))
+  }
+  return merge({}, config, require('./prod'))
+}

+ 22 - 0
config/prod.js

@@ -0,0 +1,22 @@
+const isH5 = process.env.CLIENT_ENV === 'h5'
+
+const HOST = '"https://miniapp.you.163.com"'
+const HOST_M = '"https://m.you.163.com"'
+
+// XXX 搭了个 proxy 用于演示 prod 环境的 H5
+const HOST_H5 = '"http://140.210.193.73"'
+const HOST_M_H5 = '"http://jsnewbee.com/taro-yanxuan/api-m"'
+
+module.exports = {
+  env: {
+    NODE_ENV: '"production"'
+  },
+  defineConstants: {
+    HOST: isH5 ? HOST_H5 : HOST,
+    HOST_M: isH5 ? HOST_M_H5 : HOST_M
+  },
+  weapp: {},
+  h5: {
+    publicPath: '/taro-yanxuan'
+  }
+}

+ 71 - 0
package.json

@@ -0,0 +1,71 @@
+{
+	"name": "taro-yanxuan",
+	"version": "1.3.0",
+	"private": true,
+	"description": "Taro 多端(微信小程序 + H5 + React Native)",
+	"scripts": {
+		"build:weapp": "taro build --type weapp",
+		"build:swan": "taro build --type swan",
+		"build:alipay": "taro build --type alipay",
+		"build:tt": "taro build --type tt",
+		"build:h5": "cross-env CLIENT_ENV=h5 taro build --type h5",
+		"build:rn": "cross-env CLIENT_ENV=rn taro build --type rn",
+		"dev:weapp": "npm run build:weapp -- --watch",
+		"dev:swan": "npm run build:swan -- --watch",
+		"dev:alipay": "npm run build:alipay -- --watch",
+		"dev:tt": "npm run build:tt -- --watch",
+		"dev:h5": "cross-env CLIENT_ENV=h5 npm run build:h5 -- --watch",
+		"dev:rn": "cross-env CLIENT_ENV=rn npm run build:rn -- --watch"
+	},
+	"author": "js-newbee",
+	"license": "MIT",
+	"dependencies": {
+		"@tarojs/async-await": "1.3.4",
+		"@tarojs/components": "1.3.4",
+		"@tarojs/components-rn": "1.3.4",
+		"@tarojs/redux": "1.3.4",
+		"@tarojs/redux-h5": "1.3.4",
+		"@tarojs/rn-runner": "1.3.4",
+		"@tarojs/router": "1.3.4",
+		"@tarojs/taro": "1.3.4",
+		"@tarojs/taro-alipay": "1.3.4",
+		"@tarojs/taro-h5": "1.3.4",
+		"@tarojs/taro-redux-rn": "1.3.4",
+		"@tarojs/taro-rn": "1.3.4",
+		"@tarojs/taro-router-rn": "1.3.4",
+		"@tarojs/taro-swan": "1.3.4",
+		"@tarojs/taro-tt": "1.3.4",
+		"@tarojs/taro-weapp": "1.3.4",
+		"blueimp-md5": "^2.10.0",
+		"nerv-devtools": "^1.4.3",
+		"nervjs": "^1.4.3",
+		"react": "16.3.1",
+		"react-native": "0.55.4",
+		"redux": "^4.0.0",
+		"redux-logger": "^3.0.6",
+		"redux-thunk": "^2.3.0",
+		"tslib": "^1.8.0"
+	},
+	"devDependencies": {
+		"@tarojs/cli": "1.3.4",
+		"@tarojs/plugin-babel": "1.3.4",
+		"@tarojs/plugin-csso": "1.3.4",
+		"@tarojs/plugin-sass": "1.3.4",
+		"@tarojs/plugin-uglifyjs": "1.3.4",
+		"@tarojs/webpack-runner": "1.3.4",
+		"@types/react": "^16.4.8",
+		"@types/webpack-env": "^1.13.6",
+		"babel-eslint": "^8.2.3",
+		"babel-plugin-transform-class-properties": "^6.24.1",
+		"babel-plugin-transform-decorators-legacy": "^1.3.4",
+		"babel-plugin-transform-jsx-stylesheet": "^0.6.5",
+		"babel-plugin-transform-object-rest-spread": "^6.26.0",
+		"babel-preset-env": "^1.6.1",
+		"cross-env": "^5.2.0",
+		"eslint": "^4.19.1",
+		"eslint-config-taro": "1.3.4",
+		"eslint-plugin-import": "^2.12.0",
+		"eslint-plugin-react": "^7.8.2",
+		"eslint-plugin-taro": "1.3.4"
+	}
+}

+ 16 - 0
project.config.json

@@ -0,0 +1,16 @@
+{
+	"miniprogramRoot": "dist/",
+	"projectname": "taro-yanxuan",
+	"description": "Taro 多端实践之网易严选(微信小程序 + H5 + React Native)",
+	"appid": "touristappid",
+	"setting": {
+		"urlCheck": false,
+		"es6": false,
+		"postcss": false,
+		"minified": false,
+		"newFeature": true,
+		"autoAudits": false
+	},
+	"compileType": "miniprogram",
+	"condition": {}
+}

+ 16 - 0
src/actions/increase.js

@@ -0,0 +1,16 @@
+import {} from '@constants/increase'
+import {
+  API_HOME, API_HOME_SEARCH_COUNT, API_HOME_RECOMMEND, API_HOME_PIN
+} from '@constants/api'
+import { createAction } from '@utils/redux'
+
+/**
+ * 首页数据
+ * @param {*} payload
+ */
+// export const dispatchHome = payload => createAction({
+//   url: API_HOME,
+//   type: HOME_INFO,
+//   payload
+// })
+

+ 16 - 0
src/actions/list.js

@@ -0,0 +1,16 @@
+import { } from '@constants/list'
+import {
+  API_HOME, API_HOME_SEARCH_COUNT, API_HOME_RECOMMEND, API_HOME_PIN
+} from '@constants/api'
+import { createAction } from '@utils/redux'
+
+/**
+ * 首页数据
+ * @param {*} payload
+ */
+// export const dispatchHome = payload => createAction({
+//   url: API_HOME,
+//   type: HOME_INFO,
+//   payload
+// })
+

+ 15 - 0
src/actions/user.js

@@ -0,0 +1,15 @@
+import { USER_LOGIN } from '@constants/user'
+import { API_USER_LOGIN } from '@constants/api'
+import { createAction } from '@utils/redux'
+
+
+/**
+ * 用户登录
+ * @param {*} payload
+ */
+export const dispatchLogin = payload => createAction({
+  url: API_USER_LOGIN,
+  method: 'POST',
+  type: USER_LOGIN,
+  payload
+})

+ 78 - 0
src/app.js

@@ -0,0 +1,78 @@
+import '@tarojs/async-await'
+import Taro, { Component } from '@tarojs/taro'
+import { Provider } from '@tarojs/redux'
+
+import Index from './pages/index'
+
+import configStore from './store'
+
+import './app.scss'
+
+// 如果需要在 h5 环境中开启 React Devtools
+// 取消以下注释:
+// if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5')  {
+//   require('nerv-devtools')
+// }
+
+const store = configStore()
+
+class App extends Component {
+  config = {
+    pages: [
+      'pages/user-login/user-login',
+      'pages/list/list',
+      'pages/increase/increase',
+      'pages/user/user',
+    ],
+    window: {
+      backgroundTextStyle: 'light',
+      navigationBarBackgroundColor: '#fff',
+      navigationBarTitleText: 'RD-Al生成',
+      navigationBarTextStyle: 'black'
+    },
+    tabBar: {
+      color: "#666",
+      selectedColor: "#b4282d",
+      backgroundColor: "#fafafa",
+      borderStyle: 'black',
+      list: [{
+        pagePath: "pages/list/list",
+        iconPath: "./assets/tab-bar/home.png",
+        selectedIconPath: "./assets/tab-bar/home-active.png",
+        text: "报告列表"
+      }, {
+        pagePath: "pages/increase/increase",
+        iconPath: "./assets/tab-bar/cate.png",
+        selectedIconPath: "./assets/tab-bar/cate-active.png",
+        text: "+生成报告"
+      },{
+        pagePath: "pages/user/user",
+        iconPath: "./assets/tab-bar/user.png",
+        selectedIconPath: "./assets/tab-bar/user-active.png",
+        text: "我的"
+      }]
+    }
+  }
+
+  componentDidMount () {}
+
+  componentDidShow () {}
+
+  componentDidHide () {}
+
+  componentCatchError () {}
+
+  componentDidCatchError () {}
+
+  // 在 App 类中的 render() 函数没有实际作用
+  // 请勿修改此函数
+  render () {
+    return (
+      <Provider store={store}>
+        <Index />
+      </Provider>
+    )
+  }
+}
+
+Taro.render(<App />, document.getElementById('app'))

+ 1 - 0
src/app.scss

@@ -0,0 +1 @@
+@import '@styles/reset.scss';

BIN
src/assets/default-avatar.png


BIN
src/assets/logo.acd490fa.png


BIN
src/assets/tab-bar/cart-active.png


BIN
src/assets/tab-bar/cart.png


BIN
src/assets/tab-bar/cate-active.png


BIN
src/assets/tab-bar/cate.png


BIN
src/assets/tab-bar/home-active.png


BIN
src/assets/tab-bar/home.png


BIN
src/assets/tab-bar/user-active.png


BIN
src/assets/tab-bar/user.png


+ 54 - 0
src/components/button/index.js

@@ -0,0 +1,54 @@
+import Taro, { Component } from '@tarojs/taro'
+import { Button, Text } from '@tarojs/components'
+import { postcss } from '@utils/style'
+import classNames from 'classnames'
+import './index.scss'
+
+export default class ButtonItem extends Component {
+  static defaultProps = {
+    compStyle: '',
+    textStyle: '',
+    openType: '',
+    plain: false,
+    loading: false,
+    disabled: false,
+    onClick: () => {},
+    onGetUserInfo: () => {}
+  }
+
+  getCls = (base) => {
+    const { type, plain, disabled } = this.props
+    return classNames(
+      base,
+      type === 'primary' && `${base}--primary`,
+      plain && `${base}--plain`,
+      disabled && `${base}--disabled`
+    )
+  }
+
+  render () {
+    const {
+      compStyle, textStyle, openType, loading, disabled, text,
+      onClick, onGetUserInfo
+    } = this.props
+    return (
+      <Button
+        className={this.getCls('comp-button')}
+        style={postcss(compStyle)}
+        loading={loading}
+        disabled={disabled}
+        openType={openType}
+        onClick={onClick}
+        onGetUserInfo={onGetUserInfo}
+      >
+        <Text
+          className={this.getCls('comp-button__txt')}
+          style={textStyle}
+        >
+          {text}
+        </Text>
+      </Button>
+    )
+  }
+}
+

+ 43 - 0
src/components/button/index.scss

@@ -0,0 +1,43 @@
+@import '@styles/theme.scss';
+
+.comp-button {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 94px;
+  border: 0;
+  border-radius: 0;
+  background: #ffffff;
+
+  &--primary {
+    background: $primary-color;
+  }
+
+  &--plain {
+    border: 1px solid $primary-color;
+    /* TODO background: transparent 的编译有问题 */
+    background-color: transparent;
+  }
+
+  &--disabled {
+    background: $disabled-color;
+  }
+}
+
+.comp-button__txt {
+  color: $text-color;
+  font-size: 28px;
+
+  &--primary {
+    color: #ffffff;
+  }
+
+  &--plain {
+    color: $primary-color;
+  }
+
+  &--disabled {
+    color: #ffffff;
+  }
+}

BIN
src/components/checkbox/assets/checked.png


BIN
src/components/checkbox/assets/un-checked.png


+ 30 - 0
src/components/checkbox/index.js

@@ -0,0 +1,30 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Image } from '@tarojs/components'
+import checkedIcon from './assets/checked.png'
+import unCheckedIcon from './assets/un-checked.png'
+import './index.scss'
+
+export default class CheckBoxItem extends Component {
+  static defaultProps = {
+    compStyle: '',
+    checked: false,
+    onClick: () => {}
+  }
+
+  render () {
+    const { compStyle, checked } = this.props
+    return (
+      <View
+        className='comp-checkbox'
+        style={compStyle}
+        onClick={this.props.onClick}
+      >
+        <Image
+          className='comp-checkbox__img'
+          src={checked ? checkedIcon : unCheckedIcon}
+        />
+        {this.props.children}
+      </View>
+    )
+  }
+}

+ 13 - 0
src/components/checkbox/index.scss

@@ -0,0 +1,13 @@
+@import '@styles/theme.scss';
+
+.comp-checkbox {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.comp-checkbox__img {
+  width: 40px;
+  height: 40px;
+  margin-right: 26px;
+}

BIN
src/components/home-title/aseets/arrow-right.png


+ 30 - 0
src/components/home-title/index.js

@@ -0,0 +1,30 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Text, Image } from '@tarojs/components'
+import arrowRight from './aseets/arrow-right.png'
+import './index.scss'
+
+export default class HomeTitle extends Component {
+  static defaultProps = {
+    title: '',
+    link: ''
+  }
+
+  render () {
+    const { title, link } = this.props
+    return (
+      <View className='comp-home-title'>
+        <Text className='comp-home-title__txt'>{title}</Text>
+        <View className='comp-home-title__content'>
+          {this.props.children}
+        </View>
+        {!!link &&
+          <View className='comp-home-title__link'>
+            <Text className='comp-home-title__link-txt'>更多</Text>
+            <Image className='comp-home-title__link-img' src={arrowRight} />
+          </View>
+        }
+      </View>
+    )
+  }
+}
+

+ 36 - 0
src/components/home-title/index.scss

@@ -0,0 +1,36 @@
+@import '@styles/theme.scss';
+
+.comp-home-title {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  height: 96px;
+}
+
+.comp-home-title__txt {
+  color: $black;
+  font-size: 32px;
+  font-weight: bold;
+}
+
+.comp-home-title__content {
+  flex: 1;
+}
+
+.comp-home-title__link {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  &-txt {
+    font-size: 26px;
+    line-height: 26px;
+    color: $text-color-light;
+  }
+
+  &-img {
+    margin-left: 10px;
+    width: 14px;
+    height: 24px;
+  }
+}

+ 9 - 0
src/components/index.js

@@ -0,0 +1,9 @@
+export { default as CheckboxItem } from './checkbox'
+export { default as ButtonItem } from './button'
+export { default as HomeTitle } from './home-title'
+export { default as InputItem } from './input-item'
+export { default as InputNumber } from './input-number'
+export { default as ItemList } from './item-list'
+export { default as Loading } from './loading'
+export { default as Popup } from './popup'
+export { default as Tag } from './tag'

BIN
src/components/input-item/assets/clear.png


+ 59 - 0
src/components/input-item/index.js

@@ -0,0 +1,59 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Image, Input } from '@tarojs/components'
+import iconClose from './assets/clear.png'
+import './index.scss'
+
+export default class InputItem extends Component {
+  static defaultProps = {
+    type: 'text',
+    value: '',
+    placeholder: '',
+    focus: false,
+    password: false,
+    compStyle: '',
+    inputStyle: '',
+    onInput: () => {},
+    onFocus: () => {},
+    onBlur: () => {}
+  }
+
+  handleInput = (e) => {
+    this.props.onInput(e.detail.value)
+  }
+
+  handleClear = () => {
+    this.props.onInput('')
+  }
+
+  render () {
+    const {
+      type, value, placeholder, focus, password,
+      compStyle, inputStyle,
+      onFocus, onBlur
+    } = this.props
+
+    return (
+      <View className='comp-input-item' style={compStyle}>
+        <Input
+          className='comp-input-item__input'
+          style={inputStyle}
+          type={type}
+          value={value}
+          focus={focus}
+          password={password}
+          placeholder={placeholder}
+          placeholderClass='comp-input-item__input--placeholder'
+          onInput={this.handleInput}
+          onFocus={onFocus}
+          onBlur={onBlur}
+        />
+        {!!value &&
+          <View className='comp-input-item__clear' onClick={this.handleClear}>
+            <Image className='comp-input-item__clear-img' src={iconClose} />
+          </View>
+        }
+      </View>
+    )
+  }
+}
+

+ 37 - 0
src/components/input-item/index.scss

@@ -0,0 +1,37 @@
+@import '@styles/theme.scss';
+
+$height: 106px;
+
+.comp-input-item {
+  position: relative;
+  border: 0 solid #d8d8d8;
+  border-bottom-width: 1px;
+}
+
+.comp-input-item__input {
+  height: $height;
+  color: $black;
+  font-size: 24px;
+
+  &--placeholder {
+    color: $text-color-light;
+  }
+}
+
+.comp-input-item__clear {
+  position: absolute;
+  top: 0;
+  right: 0;
+  z-index: 2;
+  width: 42px;
+  height: $height;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+
+  &-img {
+    width: 30px;
+    height: 30px;
+  }
+}

BIN
src/components/input-number/assets/minus-disabled.png


BIN
src/components/input-number/assets/minus.png


BIN
src/components/input-number/assets/plus.png


+ 63 - 0
src/components/input-number/index.js

@@ -0,0 +1,63 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Image, Text } from '@tarojs/components'
+import classNames from 'classnames'
+import minusIcon from './assets/minus.png'
+import minusDisabledIcon from './assets/minus-disabled.png'
+import plusIcon from './assets/plus.png'
+import './index.scss'
+
+export default class InputNumber extends Component {
+  static defaultProps = {
+    num: 0,
+    compStyle: '',
+    optStyle: '',
+    numStyle: '',
+    onChange: () => {}
+  }
+
+  handleMinus = () => {
+    if (this.props.num > 1) {
+      this.props.onChange(this.props.num - 1)
+    }
+  }
+
+  handlePlus = () => {
+    this.props.onChange(this.props.num + 1)
+  }
+
+  render () {
+    const { num, compStyle, numStyle } = this.props
+    const isMinusDisabled = num <= 1
+    return (
+      <View className='comp-input-number' style={compStyle}>
+        <View
+          className={classNames('comp-input-number__minus',
+            isMinusDisabled && 'comp-input-number__minus--disabled'
+          )}
+          onClick={this.handleMinus}
+        >
+          <Image
+            className='comp-input-number__minus-img'
+            src={isMinusDisabled ? minusDisabledIcon : minusIcon}
+          />
+        </View>
+        <View
+          className='comp-input-number__num'
+          style={numStyle}
+        >
+          <Text className='comp-input-number__num-txt'>{num}</Text>
+        </View>
+        <View
+          className='comp-input-number__plus'
+          onClick={this.handlePlus}
+        >
+          <Image
+            className='comp-input-number__plus-img'
+            src={plusIcon}
+          />
+        </View>
+      </View>
+    )
+  }
+}
+

+ 52 - 0
src/components/input-number/index.scss

@@ -0,0 +1,52 @@
+@import '@styles/theme.scss';
+
+.comp-input-number {
+  display: flex;
+  flex-direction: row;
+  height: 52px;
+}
+
+.comp-input-number__minus,
+.comp-input-number__plus {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  width: 84px;
+  height: 100%;
+  border: 1px solid #7f7f7f;
+  @include hairline(border-width);
+  border-left-width: 0;
+  border-radius: 0 $border-radius $border-radius 0;
+
+  &-img {
+    width: 30px;
+    height: 30px;
+  }
+}
+
+.comp-input-number__minus {
+  @include hairline(border-left-width);
+  border-right-width: 0;
+  border-radius: $border-radius 0 0 $border-radius;
+
+  &--disabled {
+    border-color: #cccccc;
+  }
+}
+
+.comp-input-number__num {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  width: 76px;
+  height: 100%;
+  border: 1px solid #7f7f7f;
+
+  &-txt {
+    color: $text-color;
+    font-size: 24px;
+    line-height: 24px;
+  }
+}

+ 57 - 0
src/components/item-list/index.js

@@ -0,0 +1,57 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Text, Image } from '@tarojs/components'
+import { Tag } from '@components'
+import './index.scss'
+
+export default class ItemList extends Component {
+  static defaultProps = {
+    list: []
+  }
+
+  handleClick = (item) => {
+    Taro.navigateTo({
+      url: `/pages/item/item?itemId=${item.id}`
+    })
+  }
+
+  render () {
+    const { list } = this.props
+    return (
+      <View className='comp-item-list'>
+        <View className='comp-item-list__title'>
+          {this.props.children}
+        </View>
+        <View className='comp-item-list__wrap'>
+          {list.map(item => (
+            <View
+              key={item.id}
+              className='comp-item-list__item'
+              onClick={this.handleClick.bind(this, item)}
+            >
+              <Image className='comp-item-list__item-img' src={item.listPicUrl} />
+              <View className='comp-item-list__item-info'>
+                {!!item.limitedTag &&
+                  <Tag text={item.limitedTag} />
+                }
+                <Text className='comp-item-list__item-name' numberOfLines={1}>
+                  {item.name}
+                </Text>
+
+                <View className='comp-item-list__item-price-wrap'>
+                  <Text className='comp-item-list__item-price'>
+                    ¥{item.activityPrice || item.retailPrice}
+                  </Text>
+                  {!!item.activityPrice &&
+                    <Text className='comp-item-list__item-price--origin'>
+                      ¥{item.retailPrice}
+                    </Text>
+                  }
+                </View>
+              </View>
+            </View>
+          ))}
+        </View>
+      </View>
+    )
+  }
+}

+ 70 - 0
src/components/item-list/index.scss

@@ -0,0 +1,70 @@
+@import "@styles/theme.scss";
+
+.comp-item-list {
+  margin-top: 20px;
+  background: #ffffff;
+}
+
+.comp-item-list__title {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  padding: 0 30px;
+  height: 100px;
+}
+
+.comp-item-list__wrap {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  padding: 0 15px;
+}
+
+.comp-item-list__item {
+  display: flex;
+  flex-direction: column;
+  padding: 0 15px 48px;
+  width: 50%;
+
+  &-img {
+    width: 330px;
+    height: 330px;
+    background: #f4f4f4;
+    border-radius: 4px;
+  }
+
+  &-info {
+    margin-top: 20px;
+    display: flex;
+    flex-direction: column;
+  }
+
+  &-name {
+    color: $text-color;
+    font-size: 28px;
+    line-height: 40px;
+    @include text-ellipsis;
+  }
+
+  &-price-wrap {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+  }
+
+  &-price {
+    color: $black;
+    font-size: 32px;
+    line-height: 48px;
+  }
+
+  &-price--origin {
+    margin-left: 14px;
+    color: $text-color-lighter;
+    font-size: 30px;
+    font-weight: 200;
+    line-height: 48px;
+    text-decoration: line-through;
+    text-decoration-color: $text-color-lighter;
+  }
+}

BIN
src/components/loading/assets/loading.gif


+ 15 - 0
src/components/loading/index.js

@@ -0,0 +1,15 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Image } from '@tarojs/components'
+import loading from './assets/loading.gif'
+import './index.scss'
+
+export default class Loading extends Component {
+  render () {
+    return (
+      <View className='comp-loading'>
+        <Image src={loading} className='comp-loading__img' />
+      </View>
+    )
+  }
+}
+

+ 14 - 0
src/components/loading/index.scss

@@ -0,0 +1,14 @@
+@import '@styles/theme.scss';
+
+.comp-loading {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  height: 120px;
+}
+
+.comp-loading__img {
+  width: 50px;
+  height: 50px;
+}

BIN
src/components/popup/assets/close.png


+ 66 - 0
src/components/popup/index.js

@@ -0,0 +1,66 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, ScrollView, Image } from '@tarojs/components'
+import classNames from 'classnames'
+import closeIcon from './assets/close.png'
+import './index.scss'
+
+export default class Popup extends Component {
+  static defaultProps = {
+    visible: false,
+    compStyle: '',
+    onClose: () => {}
+  }
+
+  constructor (props) {
+    super(props)
+    this.state = {
+      isShow: props.visible
+    }
+  }
+
+  componentWillReceiveProps (nextProps) {
+    const { visible } = nextProps
+    const { isShow } = this.state
+    if (visible !== isShow) {
+      this.setState({
+        isShow: visible
+      })
+    }
+  }
+
+  handleClose = () => {
+    this.props.onClose()
+  }
+
+  handleTouchMove = e => {
+    e.stopPropagation()
+  }
+
+  render () {
+    const { onClose, compStyle } = this.props
+    const { isShow } = this.state
+
+    return (
+      <View
+        className={classNames('comp-popup', isShow && 'comp-popup--visible')}
+        onTouchMove={this.handleTouchMove}
+        style={compStyle}
+      >
+        <View className='comp-popup__mask' onClick={onClose} />
+        <View className='comp-popup__wrapper'>
+          <ScrollView
+            scrollY
+            className='comp-popup__content'
+            style={{ height: Taro.pxTransform(750) }}
+          >
+            {this.props.children}
+          </ScrollView>
+          <View className='comp-popup__close' onClick={onClose}>
+            <Image className='comp-popup__close-img' src={closeIcon} />
+          </View>
+        </View>
+      </View>
+    )
+  }
+}
+

+ 51 - 0
src/components/popup/index.scss

@@ -0,0 +1,51 @@
+@import '@styles/theme.scss';
+
+.comp-popup {
+  position: absolute;
+  width: 100%;
+  height: 0;
+  top: 0;
+  left: 0;
+  z-index: 9;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+
+  // NOTE RN 没有 display: none,暂时用 height 实现同等效果
+  &--visible {
+    height: 100%;
+  }
+}
+
+.comp-popup__mask {
+  flex: 1;
+  width: 100%;
+  background: rgba(0, 0, 0, 0.25);
+}
+
+.comp-popup__wrapper {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  background: #ffffff;
+}
+
+.comp-popup__content {
+}
+
+.comp-popup__close {
+  position: absolute;
+  top: 22px;
+  right: 22px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  width: 48px;
+  height: 48px;
+
+  &-img {
+    width: 24px;
+    height: 24px;
+  }
+}

+ 37 - 0
src/components/tag/index.js

@@ -0,0 +1,37 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Text } from '@tarojs/components'
+import classNames from 'classnames'
+import './index.scss'
+
+export default class Tag extends Component {
+  static defaultProps = {
+    compStyle: '',
+    textStyle: ''
+  }
+
+  getCls = (base) => {
+    const { size } = this.props
+    return classNames(
+      base,
+      size && `${base}--size-${size}`
+    )
+  }
+
+  render () {
+    const { compStyle, textStyle, text } = this.props
+    return (
+      <View
+        className={this.getCls('comp-tag')}
+        style={compStyle}
+      >
+        <Text
+          className={this.getCls('comp-tag__txt')}
+          style={textStyle}
+        >
+          {text}
+        </Text>
+      </View>
+    )
+  }
+}
+

+ 17 - 0
src/components/tag/index.scss

@@ -0,0 +1,17 @@
+@import '@styles/theme.scss';
+
+.comp-tag {
+  display: flex;
+  flex-direction: row;
+  margin-bottom: 10px;
+}
+
+.comp-tag__txt {
+  padding: 6px 18px;
+  color: #ffffff;
+  font-size: 24px;
+  font-weight: 200;
+  line-height: 24px;
+  background: #e37b7d;
+  border-radius: 2px;
+}

+ 16 - 0
src/constants/api.js

@@ -0,0 +1,16 @@
+/**
+ * NOTE HOST、HOST_M 是在 config 中通过 defineConstants 配置的
+ * 只所以不在代码中直接引用,是因为 eslint 会报 no-undef 的错误,因此用如下方式处理
+ */
+/* eslint-disable */
+export const host = HOST
+export const hostM = HOST_M
+/* eslint-enable */
+
+// pic
+export const CDN = 'https://yanxuan.nosdn.127.net'
+
+// user
+export const API_USER_LOGIN = `${host}/gw/user/login` //
+
+

+ 1 - 0
src/constants/increase.js

@@ -0,0 +1 @@
+export const INCREASE_MENU = 'INCREASE_MENU'

+ 1 - 0
src/constants/list.js

@@ -0,0 +1 @@
+export const LIST_MENU = 'LIST_MENU'

+ 3 - 0
src/constants/user.js

@@ -0,0 +1,3 @@
+export const USER_INFO = 'USER_INFO'
+export const USER_LOGIN = 'USER_LOGIN'
+export const USER_LOGOUT = 'USER_LOGOUT'

+ 19 - 0
src/index.html

@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
+  <meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-touch-fullscreen" content="yes">
+  <meta name="format-detection" content="telephone=no,address=no">
+  <meta name="apple-mobile-web-app-status-bar-style" content="white">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
+  <title>Taro</title>
+  <script>
+    !function(x){function w(){var v,u,t,tes,s=x.document,r=s.documentElement,a=r.getBoundingClientRect().width;if(!v&&!u){var n=!!x.navigator.appVersion.match(/AppleWebKit.*Mobile.*/);v=x.devicePixelRatio;tes=x.devicePixelRatio;v=n?v:1,u=1/v}if(a>=640){r.style.fontSize="40px"}else{if(a<=320){r.style.fontSize="20px"}else{r.style.fontSize=a/320*20+"px"}}}x.addEventListener("resize",function(){w()});w()}(window);
+  </script>
+</head>
+<body>
+  <div id="app"></div>
+</body>
+</html>

+ 58 - 0
src/pages/increase/increase.js

@@ -0,0 +1,58 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Text, Image, ScrollView } from '@tarojs/components'
+import { Loading } from '@components'
+import { connect } from '@tarojs/redux'
+import * as actions from '@actions/increase'
+import { getWindowHeight } from '@utils/style'
+import './increase.scss'
+
+const RECOMMEND_SIZE = 20
+
+@connect(state => state.home, { ...actions })
+class Increase extends Component {
+  config = {
+    navigationBarTitleText: '生成报告'
+  }
+
+  state = {
+    loaded: false,
+    loading: false,
+    lastItemId: 0,
+    hasMore: true
+  }
+
+  componentDidMount() {
+    // NOTE 暂时去掉不适配的内容
+    // Taro.showToast({
+    //   title: '注意,由于严选小程序首页界面、接口大幅变动,暂时去掉不相符的部分,后续再跟进改动',
+    //   icon: 'none',
+    //   duration: 6000
+    // })
+
+  }
+
+
+  render() {
+    // if (!this.state.loaded) {
+    //   return <Loading />
+    // }
+
+    const { } = this.props
+    return (
+      <View className='home'>
+        <ScrollView
+          scrollY
+          className='home__wrap'
+          onScrollToLower={this.loadRecommend}
+          style={{ height: getWindowHeight() }}
+        >
+          <View onClick={this.handlePrevent}>
+            新建报告
+          </View>
+        </ScrollView>
+      </View>
+    )
+  }
+}
+
+export default Increase

+ 0 - 0
src/pages/increase/increase.scss


+ 43 - 0
src/pages/list/list.js

@@ -0,0 +1,43 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Text, Image, ScrollView } from '@tarojs/components'
+import { Loading } from '@components'
+import { connect } from '@tarojs/redux'
+import * as actions from '@actions/list'
+// import { dispatchCartNum } from '@actions/list'
+import { getWindowHeight } from '@utils/style'
+import './list.scss'
+
+const RECOMMEND_SIZE = 20
+
+// @connect(state => state.home, { ...actions, })
+class List extends Component {
+  config = {
+    navigationBarTitleText: '报告列表'
+  }
+
+  state = {
+    loaded: false,
+    loading: false,
+    lastItemId: 0,
+    hasMore: true
+  }
+
+  componentDidMount() {
+
+  }
+
+  render() {
+    // if (!this.state.loaded) {
+    //   return <Loading />
+    // }
+
+    const { } = this.props
+    return (
+      <View className='home'>
+        liebiao
+      </View>
+    )
+  }
+}
+
+export default List

+ 0 - 0
src/pages/list/list.scss


+ 94 - 0
src/pages/user-login/user-login.js

@@ -0,0 +1,94 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Text, Image } from '@tarojs/components'
+import md5 from 'blueimp-md5'
+import { connect } from '@tarojs/redux'
+import * as actions from '@actions/user'
+import { ButtonItem, InputItem } from '@components'
+import { CDN } from '@constants/api'
+import './user-login.scss'
+import logo from '../../assets/logo.acd490fa.png'
+
+@connect(state => state.user, actions)
+class UserLoginEmail extends Component {
+  config = {
+    navigationBarTitleText: '登录'
+  }
+
+  state = {
+    username: '',
+    password: '',
+    loading: false
+  }
+
+  componentDidMount() {
+
+  }
+
+  handleInput = (key, value) => {
+    this.setState({ [key]: value })
+  }
+
+
+  handleLogin = () => {
+    const payload = {
+      mobilePhone: this.state.username,
+      password: this.state.password,
+      type: 1
+    }
+    this.setState({ loading: true })
+    this.props.dispatchLogin(payload).then(() => {
+      this.setState({ loading: false })
+
+      Taro.switchTab({
+        url: '/pages/list/list',
+      })
+
+    }).catch(() => {
+      this.setState({ loading: false })
+    })
+  }
+
+  render() {
+    const { username, password, loading } = this.state
+    const isBtnDisabled = !username || !password
+
+    // XXX 暂未实现 input 的 autoFocus 的逻辑
+    return (
+      <View className='user-login-email'>
+        <View className='user-login-email__logo'>
+          <Image src={logo} className='user-login-email__logo-img' />
+        </View>
+        <View className='user-login-email__wrap'>
+          <InputItem
+            value={username}
+            placeholder='用户名'
+            onInput={this.handleInput.bind(this, 'username')}
+          />
+          <InputItem
+            password
+            value={password}
+            placeholder='密码'
+            onInput={this.handleInput.bind(this, 'password')}
+          />
+        </View>
+        <View className='user-login-email__btn'>
+          <ButtonItem
+            text='登录'
+            disabled={isBtnDisabled}
+            loading={loading}
+            onClick={this.handleLogin}
+            compStyle={{
+              background: '#0079C2',
+              borderRadius: Taro.pxTransform(4)
+            }}
+            textStyle={{
+              color: isBtnDisabled ? 'rgba(255, 255, 255, 0.4)' : '#ffffff'
+            }}
+          />
+        </View>
+      </View>
+    )
+  }
+}
+
+export default UserLoginEmail

+ 54 - 0
src/pages/user-login/user-login.scss

@@ -0,0 +1,54 @@
+@import '@styles/theme.scss';
+
+.user-login-email {
+  display: flex;
+  flex-direction: column;
+  min-height: 100%;
+  padding: 240px 40px 700px;
+  background: #ffffff;
+}
+
+.user-login-email__logo {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  margin-bottom: 100px;
+
+  &-img {
+    // width: 268px;
+    height: 100px;
+    // border: 1px solid red;
+  }
+}
+
+.user-login-email__wrap {
+  position: relative;
+  z-index: 2;
+}
+
+.user-login-email__suggest {
+  position: absolute;
+  top: 108px;
+  left: 0;
+  z-index: 9;
+  width: 100%;
+  background: #ffffff;
+
+  &-item {
+    height: 80px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+
+    &-txt {
+      color: $text-color;
+      font-size: 24px;
+    }
+  }
+}
+
+.user-login-email__btn {
+  position: relative;
+  margin-top: 60px;
+  z-index: 1;
+}

+ 43 - 0
src/pages/user/user.js

@@ -0,0 +1,43 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, Text, Image, ScrollView } from '@tarojs/components'
+import { Loading } from '@components'
+import { connect } from '@tarojs/redux'
+import * as actions from '@actions/user'
+import { getWindowHeight } from '@utils/style'
+import './user.scss'
+
+const RECOMMEND_SIZE = 20
+
+@connect(state => state.home, { ...actions, })
+class User extends Component {
+  config = {
+    navigationBarTitleText: '我的'
+  }
+
+  state = {
+    loaded: false,
+    loading: false,
+    lastItemId: 0,
+    hasMore: true
+  }
+
+  componentDidMount() {
+
+  }
+
+
+  render() {
+    if (!this.state.loaded) {
+      return <Loading />
+    }
+
+    const { } = this.props
+    return (
+      <View className='home'>
+        yyyyyyyyyy
+      </View>
+    )
+  }
+}
+
+export default User

+ 0 - 0
src/pages/user/user.scss


+ 17 - 0
src/pages/webview/rn/index.js

@@ -0,0 +1,17 @@
+/**
+ * React Native 原生组件
+ */
+import Taro, { Component } from '@tarojs/taro'
+import { WebView } from 'react-native'
+
+export default class WebViewRN extends Component {
+  render() {
+    return (
+      <WebView
+        style={{ height: '100%' }}
+        originWhitelist={['*']}
+        source={{ uri: this.props.src }}
+      />
+    )
+  }
+}

+ 41 - 0
src/pages/webview/webview.js

@@ -0,0 +1,41 @@
+import Taro, { Component } from '@tarojs/taro'
+import { View, WebView } from '@tarojs/components'
+/**
+ * // NOTE Taro 的 RN 端还未提供 WebView 组件,可以引入原生组件来解决
+ * (备注:Taro v1.2.16 已支持,这块代码还是保留作为演示)
+ *
+ * Taro 有开启了 tree shaking,对于未用到的内容编译时会自动去除
+ * 因此可以把相应端的内容都 import 进来,再根据环境进行调用即可
+ *
+ * 另外 1.2.17 版本有提供了统一接口方式 https://nervjs.github.io/taro/docs/envs.html
+ * 可以参考 ./src/pages/user-login 中的实现
+ */
+import WebViewRN from './rn'
+import './webview.scss'
+
+export default class extends Component {
+  config = {
+    navigationBarTitleText: ''
+  }
+
+  url = ''
+  title = ''
+
+  componentWillMount() {
+    this.url = decodeURIComponent(this.$router.params.url || '')
+    this.title = decodeURIComponent(this.$router.params.title || '')
+    Taro.setNavigationBarTitle({ title: this.title })
+  }
+
+  render () {
+    return (
+      <View className='webview'>
+        {/* // NOTE 编译时只会保留相应端的内容,因此非 RN 端时不会编译 WebViewRN */}
+        {process.env.TARO_ENV === 'rn' ?
+          <WebViewRN src={this.url} /> :
+          <WebView src={this.url} />
+        }
+      </View>
+    )
+  }
+}

+ 3 - 0
src/pages/webview/webview.scss

@@ -0,0 +1,3 @@
+.webview {
+  height: 100%;
+}

+ 0 - 0
src/reducers/increase.js


+ 10 - 0
src/reducers/index.js

@@ -0,0 +1,10 @@
+import { combineReducers } from 'redux'
+import increase from './increase'
+import list from './list'
+import user from './user'
+
+export default combineReducers({
+  increase,
+  list,
+  user
+})

+ 47 - 0
src/reducers/list.js

@@ -0,0 +1,47 @@
+// import {
+//   // HOME_INFO, HOME_SEARCH_COUNT, HOME_RECOMMEND, HOME_PIN
+// } from '@constants/list'
+
+// const INITIAL_STATE = {
+//   homeInfo: {},
+//   searchCount: 0,
+//   pin: [],
+//   recommend: []
+// }
+
+// export default function list(state = INITIAL_STATE, action) {
+//   // switch(action.type) {
+//   //   case HOME_INFO: {
+//   //     return {
+//   //       ...state,
+//   //       homeInfo: action.payload
+//   //     }
+//   //   }
+//   //   case HOME_SEARCH_COUNT: {
+//   //     return {
+//   //       ...state,
+//   //       searchCount: action.payload.count
+//   //     }
+//   //   }
+//   //   case HOME_PIN: {
+//   //     // 每3个分成一组
+//   //     const pin = []
+//   //     action.payload.forEach((item, index) => {
+//   //       const groupIndex = parseInt(index / 3)
+//   //       if (!pin[groupIndex]) {
+//   //         pin[groupIndex] = []
+//   //       }
+//   //       pin[groupIndex].push(item)
+//   //     })
+//   //     return { ...state, pin }
+//   //   }
+//   //   case HOME_RECOMMEND: {
+//   //     return {
+//   //       ...state,
+//   //       recommend: state.recommend.concat(action.payload.rcmdItemList)
+//   //     }
+//   //   }
+//   //   default:
+//   //     return state
+//   // }
+// }

+ 29 - 0
src/reducers/user.js

@@ -0,0 +1,29 @@
+import { USER_INFO, USER_LOGIN, USER_LOGOUT } from '@constants/user'
+
+const INITIAL_STATE = {
+  userInfo: {}
+}
+
+export default function user(state = INITIAL_STATE, action) {
+  switch(action.type) {
+    case USER_INFO: {
+      return {
+        ...state,
+        userInfo: {
+          ...action.payload,
+          login: true
+        }
+      }
+    }
+    case USER_LOGIN: {
+      return { ...state }
+    }
+    case USER_LOGOUT: {
+      return {
+        ...INITIAL_STATE
+      }
+    }
+    default:
+      return state
+  }
+}

+ 28 - 0
src/store/index.js

@@ -0,0 +1,28 @@
+import { createStore, applyMiddleware, compose } from 'redux'
+import thunkMiddleware from 'redux-thunk'
+import rootReducer from '../reducers'
+
+const composeEnhancers =
+  typeof window === 'object' &&
+  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?   
+    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
+      // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
+    }) : compose
+
+const middlewares = [
+  thunkMiddleware
+]
+
+if (process.env.NODE_ENV === 'development') {
+  middlewares.push(require('redux-logger').createLogger())
+}
+
+const enhancer = composeEnhancers(
+  applyMiddleware(...middlewares),
+  // other store enhancers if any
+)
+
+export default function configStore () {
+  const store = createStore(rootReducer, enhancer)
+  return store
+}

+ 60 - 0
src/styles/mixins.scss

@@ -0,0 +1,60 @@
+/**
+ * 对于部分不兼容的样式,可以通过 mixins 统一处理
+ */
+
+/**
+ * // NOTE Taro 编译成 RN 时对 border 的处理有问题
+ * RN 不支持针对某一边设置 style,即 border-bottom-style 会报错
+ * 那么 border-bottom: 1px 就需要写成如下形式:
+ * border: 0 style color; border-bottom-width: 1px;
+ */
+@mixin border($dir, $width, $style, $color) {
+  border: 0 $style $color;
+  @each $d in $dir {
+    #{border-#{$d}-width}: $width;
+  }
+}
+
+/**
+ * 对于不能打包到 RN 的样式,可以用 mixins 引入,相对美观一些
+ */
+@mixin eject($attr, $value) {
+  /*postcss-pxtransform rn eject enable*/
+  #{$attr}: $value;
+  /*postcss-pxtransform rn eject disable*/
+}
+
+/**
+ * // TODO 1px 的线在各端上实现方式不同,统一出来后续再兼容,目前注意两点:
+ * 1. Taro 中大写的 PX 不会被编译成 rpx/em,但 RN 还未兼容该写法
+ * 2. H5 中 1px(转成 rem 后实际小于 0.5px) + border-radius 会导致 border 不显示
+ */
+@mixin hairline($attr) {
+  #{$attr}: 1px;
+  @include eject($attr, 1PX);
+}
+
+/**
+ * NOTE RN 无法通过 text-overflow 实现省略号,这些代码不能打包到 RN 中
+ */
+@mixin text-ellipsis() {
+  /*postcss-pxtransform rn eject enable*/
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  /*postcss-pxtransform rn eject disable*/
+}
+
+/**
+ * NOTE 实现多行文本省略,RN 用 Text 标签的 numberOfLines,H5/小程序用 -webkit-line-clamp
+ */
+@mixin lamp-clamp($line) {
+  /*postcss-pxtransform rn eject enable*/
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: $line;
+  /* autoprefixer: ignore next */
+  -webkit-box-orient: vertical;
+  /*postcss-pxtransform rn eject disable*/
+}

+ 47 - 0
src/styles/reset.scss

@@ -0,0 +1,47 @@
+/**
+ * 统一多端基础样式
+ * 比较理想的是 View 标签统一为 display: flex; flex-direction: column
+ * 但是 H5 上内置组件样式可能会错乱,因此暂时不这样处理,有用到 flex 的地方都显式声明主轴方向即可
+ */
+
+/* weapp */
+page {
+  height: 100%;
+}
+
+view,
+text,
+scroll-view {
+  box-sizing: border-box;
+}
+
+button {
+  &:after {
+    display: none;
+  }
+}
+
+/* h5 */
+/*postcss-pxtransform rn eject enable*/
+div {
+  box-sizing: border-box;
+}
+
+button {
+  outline: none;
+}
+
+.taro-text {
+  box-sizing: border-box;
+}
+
+.taro-tabbar__panel,
+.taro_router {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.taro_page {
+  flex: 1;
+}
+/*postcss-pxtransform rn eject disable*/

+ 16 - 0
src/styles/theme.scss

@@ -0,0 +1,16 @@
+@import "./mixins.scss";
+
+$primary-color: #ab2b2b;
+$disabled-color: #dddddd;
+
+$black: #000000;
+$white: #ffffff;
+
+$text-color: #333333;
+$text-color-light: #666666;
+$text-color-lighter: #999999;
+
+$border-color: #dddddd;
+$border-radius: 4px;
+
+$image-bg: #f4f4f4;

+ 38 - 0
src/utils/jump.js

@@ -0,0 +1,38 @@
+import Taro from '@tarojs/taro'
+
+const PAGE_WEBVIEW = '/pages/webview/webview'
+
+/**
+ * // NOTE 后端返回的 url 可能是网页链接,需要在 webview 中打开
+ * 也可能是小程序自身的链接,只能用 navigate/redirect 之类的打开
+ * 就需要有个地方统一判断处理
+ */
+export default function jump(options) {
+  const { url, title = '', payload = {}, method = 'navigateTo' } = options
+
+  if (/^https?:\/\//.test(url)) {
+    Taro[method]({
+      url: urlStringify(PAGE_WEBVIEW, { url, title })
+    })
+  } else if (/^\/pages\//.test(url)) {
+    // TODO H5 不支持 switchTab,暂时 hack 下
+    if (process.env.TARO_ENV === 'h5' && method === 'switchTab') {
+      Taro.navigateBack({ delta: Taro.getCurrentPages().length - 1 })
+      setTimeout(() => { Taro.redirectTo({ url }) }, 100)
+      return
+    }
+
+    Taro[method]({
+      url: urlStringify(url, payload)
+    })
+  }
+}
+
+function urlStringify(url, payload, encode = true) {
+  const arr = Object.keys(payload).map(key =>
+    `${key}=${encode ? encodeURIComponent(payload[key]) : payload[key]}`
+  )
+
+  // NOTE 注意支付宝小程序跳转链接如果没有参数,就不要带上 ?,否则可能无法跳转
+  return arr.length ? `${url}?${arr.join('&')}` : url
+}

+ 15 - 0
src/utils/redux.js

@@ -0,0 +1,15 @@
+/**
+ * 适当封装 Redux,简化调用
+ */
+/* eslint-disable import/prefer-default-export */
+import fetch from './request'
+
+export function createAction(options) {
+  const { url, payload, method, fetchOptions, cb, type } = options
+  return (dispatch) => {
+    return fetch({ url, payload, method, ...fetchOptions }).then((res) => {
+      dispatch({ type, payload: cb ? cb(res) : res })
+      return res
+    })
+  }
+}

+ 83 - 0
src/utils/request.js

@@ -0,0 +1,83 @@
+import Taro from '@tarojs/taro'
+import { API_USER_LOGIN } from '@constants/api'
+
+const CODE_SUCCESS = '200'
+const CODE_AUTH_EXPIRED = '600'
+
+function getStorage(key) {
+  return Taro.getStorage({ key }).then(res => res.data).catch(() => '')
+}
+
+function updateStorage(data = {}) {
+  return Promise.all([
+    Taro.setStorage({ key: 'token', data: data['token'] || '' }),
+    // Taro.setStorage({ key: 'uid', data: data['uid'] || '' })
+  ])
+}
+
+/**
+ * 简易封装网络请求
+ * // NOTE 需要注意 RN 不支持 *StorageSync,此处用 async/await 解决
+ * @param {*} options
+ */
+export default async function fetch(options) {
+  const { url, payload, method = 'GET', showToast = true, autoLogin = true } = options
+  const token = await getStorage('token')
+  const header = token ? { 'Authorization': token, } : {}
+  if (method === 'POST') {
+    header['content-type'] = 'application/json'
+  }
+
+  return Taro.request({
+    url,
+    method,
+    data: payload,
+    header
+  }).then(async (res) => {
+    const { status, data } = res.data
+    if (status !== 1000) {
+      if (status === 200004) {
+        // 登录失效
+        await updateStorage({})
+      }
+      return Promise.reject(res.data)
+    }
+    // 登录调用
+    if (url === API_USER_LOGIN) {
+      await updateStorage(data)
+    }
+
+
+    // if (code !== CODE_SUCCESS) {
+    //   if (code === CODE_AUTH_EXPIRED) {
+    //     await updateStorage({})
+    //   }
+    //   return Promise.reject(res.data)
+    // }
+    // if (url === API_USER_LOGIN) {
+    //   await updateStorage(data)
+    // }
+    // // XXX 用户信息需展示 uid,但是 uid 是登录接口就返回的,比较蛋疼,暂时糅合在 fetch 中解决
+    // if (url === API_USER) {
+    //   const uid = await getStorage('uid')
+    //   return { ...data, uid }
+    // }
+
+    return data
+  }).catch((err) => {
+    if (showToast) {
+      Taro.showToast({
+        title: err && err.status === 200004 ? '登录失效' : err.msg,
+        icon: 'none'
+      })
+    }
+
+    if (err.status === 200004 && autoLogin) {
+      Taro.navigateTo({
+        url: '/pages/user-login/user-login'
+      })
+    }
+
+    return Promise.reject({ message: defaultMsg, ...err })
+  })
+}

+ 48 - 0
src/utils/style.js

@@ -0,0 +1,48 @@
+import Taro from '@tarojs/taro'
+
+const NAVIGATOR_HEIGHT = 44
+const TAB_BAR_HEIGHT = 50
+
+/**
+ * 返回屏幕可用高度
+ * // NOTE 各端返回的 windowHeight 不一定是最终可用高度(例如可能没减去 statusBar 的高度),需二次计算
+ * @param {*} showTabBar
+ */
+export function getWindowHeight(showTabBar = true) {
+  const info = Taro.getSystemInfoSync()
+  const { windowHeight, statusBarHeight, titleBarHeight } = info
+  const tabBarHeight = showTabBar ? TAB_BAR_HEIGHT : 0
+
+  if (process.env.TARO_ENV === 'rn') {
+    return windowHeight - statusBarHeight - NAVIGATOR_HEIGHT - tabBarHeight
+  }
+
+  if (process.env.TARO_ENV === 'h5') {
+    return `${windowHeight - tabBarHeight}px`
+  }
+
+  if (process.env.TARO_ENV === 'alipay') {
+    // NOTE 支付宝比较迷,windowHeight 似乎是去掉了 tabBar 高度,但无 tab 页跟 tab 页返回高度是一样的
+    return `${windowHeight - statusBarHeight - titleBarHeight + (showTabBar ? 0 : TAB_BAR_HEIGHT)}px`
+  }
+
+  return `${windowHeight}px`
+}
+
+/**
+ * // NOTE 样式在编译时会通过 postcss 进行处理,但 js 中的 style 可能需要运行时处理
+ * 例如加 prefix,或者对 RN 样式的兼容,又或是在此处统一处理 Taro.pxTransform
+ * 此处只做演示,若需要做完善点,可以考虑借助 https://github.com/postcss/postcss-js
+ */
+export function postcss(style) {
+  if (!style) {
+    return style
+  }
+  const { background, ...restStyle } = style
+  const newStyle = {}
+  if (background) {
+    // NOTE 如 RN 不支持 background,只支持 backgroundColor
+    newStyle.backgroundColor = background
+  }
+  return { ...restStyle, ...newStyle }
+}

+ 28 - 0
src/utils/taro.alipay.js

@@ -0,0 +1,28 @@
+/**
+ * // NOTE 原生支付宝小程序的 API 调用比较麻烦
+ * 改造一下以支持 promise 调用
+ */
+const TaroAlipay = {}
+const apiList = ['getAuthCode', 'getAuthUserInfo']
+
+apiList.forEach((api) => {
+  TaroAlipay[api] = (options = {}) => {
+    return new Promise((resolve, reject) => {
+      const { success, fail } = options
+      const args = {
+        ...options,
+        success: (res) => {
+          success && success(res)
+          resolve(res)
+        },
+        fail: (err) => {
+          fail && fail(err)
+          reject(err)
+        }
+      }
+      my[api](args)
+    })
+  }
+})
+
+export default TaroAlipay

File diff suppressed because it is too large
+ 14160 - 0
yarn.lock