Joeshu 3 years ago
commit
4baeb8773e
62 changed files with 16225 additions and 0 deletions
  1. 14 0
      .editorconfig
  2. 16 0
      .env.development
  3. 16 0
      .env.production
  4. 16 0
      .env.test
  5. 28 0
      .eslintrc.js
  6. 22 0
      .gitignore
  7. 1 0
      .prettierignore
  8. 42 0
      .prettierrc.js
  9. 27 0
      .vscode/settings.json
  10. 27 0
      README.md
  11. 3 0
      babel.config.js
  12. 9 0
      jsconfig.json
  13. 13534 0
      package-lock.json
  14. 56 0
      package.json
  15. 13 0
      postcss.config.js
  16. BIN
      public/favicon.ico
  17. 18 0
      public/index.html
  18. 21 0
      src/App.vue
  19. 44 0
      src/apis/index.js
  20. BIN
      src/assets/images/avatar_default.png
  21. BIN
      src/assets/images/empty.png
  22. BIN
      src/assets/images/icon_arrow.png
  23. BIN
      src/assets/images/icon_choose.png
  24. BIN
      src/assets/images/icon_wallet_1.png
  25. BIN
      src/assets/images/icon_wallet_2.png
  26. BIN
      src/assets/images/tabbar/good.png
  27. BIN
      src/assets/images/tabbar/good_on.png
  28. BIN
      src/assets/images/tabbar/home.png
  29. BIN
      src/assets/images/tabbar/home_on.png
  30. BIN
      src/assets/images/tabbar/mine.png
  31. BIN
      src/assets/images/tabbar/mine_on.png
  32. BIN
      src/assets/images/tabbar/order.png
  33. BIN
      src/assets/images/tabbar/order_on.png
  34. BIN
      src/assets/images/tabbar/service.png
  35. BIN
      src/assets/images/tabbar/service_on.png
  36. BIN
      src/assets/images/tabbar/订单-灰.png
  37. BIN
      src/assets/images/tabbar/订单.png
  38. BIN
      src/assets/logo.png
  39. 55 0
      src/components/Empty/index.vue
  40. 59 0
      src/components/HelloWorld.vue
  41. 206 0
      src/components/ServiceItem/index.vue
  42. 47 0
      src/components/SpainList/index.vue
  43. 93 0
      src/components/Tabbar/index.vue
  44. 51 0
      src/components/brand.vue
  45. 31 0
      src/main.js
  46. 49 0
      src/permission.js
  47. 80 0
      src/router/index.js
  48. 31 0
      src/store/index.js
  49. 36 0
      src/styles/index.less
  50. 63 0
      src/styles/variable.less
  51. 38 0
      src/utils/constans.js
  52. 183 0
      src/utils/index.js
  53. 106 0
      src/utils/lodash.js
  54. 100 0
      src/utils/request.js
  55. 153 0
      src/utils/validate.js
  56. 3 0
      src/views/error-page/401.vue
  57. 3 0
      src/views/error-page/404.vue
  58. 157 0
      src/views/home/index.vue
  59. 176 0
      src/views/paycode/index.vue
  60. 103 0
      src/views/payresult/index.vue
  61. 406 0
      src/views/recharge/index.vue
  62. 89 0
      vue.config.js

+ 14 - 0
.editorconfig

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

+ 16 - 0
.env.development

@@ -0,0 +1,16 @@
+NODE_ENV = 'development'
+
+# 自定义环境变量
+VUE_APP_ENV = 'dev'
+
+# 网站标题
+VUE_APP_TITLE = 首页
+
+# 域名
+VUE_APP_BASE_HOST = wx.palmnest.com
+
+# 协议 + 域名
+VUE_APP_BASE_ORIGIN = //wx.palmnest.com
+
+# 基础接口
+VUE_APP_BASE_API = '//wx.palmnest.com/merge'

+ 16 - 0
.env.production

@@ -0,0 +1,16 @@
+NODE_ENV = 'production'
+
+# 自定义环境变量
+VUE_APP_ENV = 'prod'
+
+# 网站标题
+VUE_APP_TITLE = 首页
+
+# 域名
+VUE_APP_BASE_HOST = superdesk.avic-s.com
+
+# 协议 + 域名
+VUE_APP_BASE_ORIGIN = //superdesk.avic-s.com
+
+# 基础接口
+VUE_APP_BASE_API = '//superdesk.avic-s.com/merge'

+ 16 - 0
.env.test

@@ -0,0 +1,16 @@
+NODE_ENV = 'production'
+
+# 自定义环境变量
+VUE_APP_ENV = 'test'
+
+# 网站标题
+VUE_APP_TITLE = 首页
+
+# 域名
+VUE_APP_BASE_HOST = wx.palmnest.com
+
+# 协议 + 域名
+VUE_APP_BASE_ORIGIN = //wx.palmnest.com
+
+# 基础接口
+VUE_APP_BASE_API = '//wx.palmnest.com/merge'

+ 28 - 0
.eslintrc.js

@@ -0,0 +1,28 @@
+module.exports = {
+  extends: ['alloy', 'alloy/vue'],
+  root: true,
+  env: {
+    // 你的环境变量(包含多个预定义的全局变量)
+    browser: true,
+    node: true,
+    // mocha: true,
+    // jest: true,
+    // jquery: true
+  },
+  globals: {
+    // 你的全局变量(设置为 false 表示它不允许被重新赋值)
+    wx: true,
+  },
+  rules: {
+    // 自定义你的规则
+    'no-console': 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'no-invalid-this': 'off',
+    'vue/component-tags-order': [
+      'error',
+      {
+        order: [['template', 'script'], 'style'],
+      },
+    ],
+  },
+}

+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 1 - 0
.prettierignore

@@ -0,0 +1 @@
+dist

+ 42 - 0
.prettierrc.js

@@ -0,0 +1,42 @@
+// .prettierrc.js
+module.exports = {
+  // 一行最多 120 字符
+  printWidth: 120,
+  // 使用 2 个空格缩进
+  tabWidth: 2,
+  // 不使用缩进符,而使用空格
+  useTabs: false,
+  // 行尾需要有分号
+  semi: false,
+  // 使用单引号
+  singleQuote: true,
+  // 对象的 key 仅在必要时用引号
+  quoteProps: 'as-needed',
+  // jsx 不使用单引号,而使用双引号
+  jsxSingleQuote: false,
+  // 末尾需要有逗号
+  trailingComma: 'all',
+  // 大括号内的首尾需要空格
+  bracketSpacing: true,
+  // jsx 标签的反尖括号需要换行
+  jsxBracketSameLine: false,
+  // 箭头函数,只有一个参数的时候,也需要括号
+  arrowParens: 'always',
+  // 每个文件格式化的范围是文件的全部内容
+  rangeStart: 0,
+  rangeEnd: Infinity,
+  // 不需要写文件开头的 @prettier
+  requirePragma: false,
+  // 不需要自动在文件开头插入 @prettier
+  insertPragma: false,
+  // 使用默认的折行标准
+  proseWrap: 'preserve',
+  // 根据显示样式决定 html 要不要折行
+  htmlWhitespaceSensitivity: 'css',
+  // vue 文件中的 script 和 style 内不用缩进
+  vueIndentScriptAndStyle: false,
+  // 换行符使用 lf
+  endOfLine: 'lf',
+  // 格式化嵌入的内容
+  embeddedLanguageFormatting: 'auto',
+}

+ 27 - 0
.vscode/settings.json

@@ -0,0 +1,27 @@
+{
+  "editor.formatOnSave": true,
+  "editor.defaultFormatter": "esbenp.prettier-vscode",
+  "eslint.validate": ["javascript", "javascriptreact", "vue", "typescript", "typescriptreact"],
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": true
+  },
+  "vetur.format.enable": false,
+  "vetur.validation.template": false,
+  "typescript.tsdk": "node_modules/typescript/lib",
+  "cSpell.words": [
+    "avic",
+    "browserslist",
+    "flashdeliver",
+    "Lazyload",
+    "Localforage",
+    "openid",
+    "ordermeal",
+    "ordermealwx",
+    "prefetch",
+    "scroller",
+    "superdesk",
+    "tailwindcss",
+    "vant",
+    "vuex"
+  ]
+}

+ 27 - 0
README.md

@@ -0,0 +1,27 @@
+# 服务之窗 - 珠江啤酒
+
+- storage 使用原来的`localforage/xx`格式命名
+
+## 快速开始
+
+```sh
+git clone https://gogs.superdesk.cn/vue-wechat/main-frame.git -b zhupi main_zhupi
+```
+
+## 接口
+
+[小幺鸡-服务之窗-珠江](http://xiaoyaoji.superdesk.cn/plugins_doc/doc/4gixjWG9tE)
+http://wx.palmnest.com/merge/backend/#/login
+账户 sa
+Zz1212
+
+## TODO
+
+- 我的菜品地址(属于订餐服务)
+- 订单页面地址和切换(属于订餐服务和楼闪送)
+- 订餐 服务地址
+- 取餐付款码 服务地址
+
+* 首页 今日餐品接口对接
+* 我的钱包(新开发&接口对接)
+* 个人资料 查询订餐用户拥有(卡号和工号)

+ 3 - 0
babel.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  presets: ['@vue/cli-plugin-babel/preset'],
+}

+ 9 - 0
jsconfig.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules"]
+}

File diff suppressed because it is too large
+ 13534 - 0
package-lock.json


+ 56 - 0
package.json

@@ -0,0 +1,56 @@
+{
+  "name": "vue-h5-standard",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "test": "vue-cli-service build --mode test --report",
+    "build": "vue-cli-service build",
+    "inspect": "vue-cli-service inspect"
+  },
+  "dependencies": {
+    "axios": "^0.21.1",
+    "core-js": "^3.6.5",
+    "good-storage": "^1.1.1",
+    "postcss": "^7.0.35",
+    "postcss-px-to-viewport": "^1.1.1",
+    "qrcodejs2": "0.0.2",
+    "qs": "^6.10.1",
+    "vant": "^2.12.16",
+    "vue": "^2.6.11",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.8",
+    "@vue/cli-service": "~4.5.8",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^7.26.0",
+    "eslint-config-alloy": "^3.10.0",
+    "eslint-plugin-vue": "^7.9.0",
+    "husky": "^7.0.2",
+    "less": "^3.0.4",
+    "less-loader": "^5.0.0",
+    "lint-staged": "^11.1.2",
+    "node-sass": "^5.0.0",
+    "sass-loader": "^10.1.0",
+    "vue-eslint-parser": "^7.6.0",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  },
+  "lint-staged": {
+    "src/**/*.{js,vue}": [
+      "prettier --write",
+      "eslint --fix",
+      "git add"
+    ]
+  },
+  "browserslist": [
+    "Android >= 4.0",
+    "iOS >= 8.0"
+  ]
+}

+ 13 - 0
postcss.config.js

@@ -0,0 +1,13 @@
+// postcss.config.js
+module.exports = {
+  plugins: {
+    'postcss-px-to-viewport': {
+      viewportWidth: 375,
+      unitPrecision: 3,
+      viewportUnit: 'vw',
+      selectorBlackList: ['.ignore', '.hairlines', /^\.dp/, /^\.scroller/], // 这里是过滤不转换的css,支持正则,如果框架本身把单位写死支持移动端,可以通过这个过滤掉
+      minPixelValue: 1,
+      mediaQuery: false,
+    },
+  },
+}

BIN
public/favicon.ico


+ 18 - 0
public/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
+    <title><%= htmlWebpackPlugin.options.title %></title>
+    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong >
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 21 - 0
src/App.vue

@@ -0,0 +1,21 @@
+<template>
+  <div id="app">
+    <keep-alive>
+      <router-view v-if="$route.meta.keepAlive"></router-view>
+    </keep-alive>
+    <router-view v-if="!$route.meta.keepAlive"></router-view>
+  </div>
+</template>
+
+<style>
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC',
+    'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -webkit-text-size-adjust: 100%;
+  line-height: 1.4;
+  font-size: 16px;
+  color: #333;
+  background-color: #f8f8f8;
+}
+</style>

+ 44 - 0
src/apis/index.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+// import qs from 'qs'
+
+/**
+ * queryUserInfoByPhone
+ * @param {*} userMobile
+ * @param {*} data
+ * @returns
+ */
+export function queryUserInfoByPhone(userMobile, data) {
+  return request({
+    url: `api/wallet/wechat/front/integral/info?userMobile=${userMobile}`,
+    method: 'post',
+    data: data,
+  })
+}
+
+/**
+ * getOrgInfo
+ */
+export function getOrgInfo(data) {
+  return request({
+    url: `/api/wallet/wechat/front/integral/orgInfo`,
+    method: 'get',
+    params: data,
+  })
+}
+
+/**
+ * 自助充值
+ */
+export function recharge(data) {
+  return request({
+    url: `/api/wallet/wechat/front/integral/recharge`,
+    method: 'post',
+    data,
+  })
+}
+
+export default {
+  queryUserInfoByPhone,
+  getOrgInfo,
+  recharge,
+}

BIN
src/assets/images/avatar_default.png


BIN
src/assets/images/empty.png


BIN
src/assets/images/icon_arrow.png


BIN
src/assets/images/icon_choose.png


BIN
src/assets/images/icon_wallet_1.png


BIN
src/assets/images/icon_wallet_2.png


BIN
src/assets/images/tabbar/good.png


BIN
src/assets/images/tabbar/good_on.png


BIN
src/assets/images/tabbar/home.png


BIN
src/assets/images/tabbar/home_on.png


BIN
src/assets/images/tabbar/mine.png


BIN
src/assets/images/tabbar/mine_on.png


BIN
src/assets/images/tabbar/order.png


BIN
src/assets/images/tabbar/order_on.png


BIN
src/assets/images/tabbar/service.png


BIN
src/assets/images/tabbar/service_on.png


BIN
src/assets/images/tabbar/订单-灰.png


BIN
src/assets/images/tabbar/订单.png


BIN
src/assets/logo.png


+ 55 - 0
src/components/Empty/index.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="empty">
+    <div class="empty-image">
+      <img :src="img" alt="" />
+    </div>
+    <div class="empty-desc">
+      {{ desc }}
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Empty',
+  props: {
+    image: {
+      type: String,
+      default: 'default',
+    },
+    desc: {
+      type: String,
+      default: '暂无数据 ~',
+    },
+  },
+  data() {
+    return {
+      img: require('@/assets/images/empty.png'),
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.empty {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: #999999;
+  font-size: 12px;
+
+  &-image {
+    padding-top: 20vw;
+  }
+
+  &-desc {
+    padding: 16px 0;
+  }
+}
+
+.empty-image > img {
+  width: 80vw;
+  object-fit: contain;
+}
+</style>

+ 59 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="hello">
+    <h1>{{ msg }}</h1>
+    <p>
+      For a guide and recipes on how to configure / customize this project,<br>
+      check out the
+      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
+    </p>
+    <h3>Installed CLI Plugins</h3>
+    <ul>
+      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
+      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
+      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
+    </ul>
+    <h3>Essential Links</h3>
+    <ul>
+      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
+      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
+      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
+      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
+      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
+    </ul>
+    <h3>Ecosystem</h3>
+    <ul>
+      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
+      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
+      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
+      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
+      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
+    </ul>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HelloWorld',
+  props: {
+    msg: String
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped lang="less">
+h3 {
+  margin: 40px 0 0;
+}
+ul {
+  list-style-type: none;
+  padding: 0;
+}
+li {
+  display: inline-block;
+  margin: 0 10px;
+}
+a {
+  color: #42b983;
+}
+</style>

+ 206 - 0
src/components/ServiceItem/index.vue

@@ -0,0 +1,206 @@
+<template>
+  <div class="service-item-perch">
+    <template v-if="wxReady && item.linkUrlType == 2">
+      <div class="service-item">
+        <wx-open-launch-weapp :username="item.weappGhId" :path="item.weappPath">
+          <script type="text/wxtag-template">
+            <style>.media{ text-align:center; }.img { display:inline-block; width: 50px; height: 50px; object-fit: cover;}.title { font-size: 14px; height: 20px; line-height: 20px; margin-top: 3px; color:#2c3e50; text-align:center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }</style>
+            <div class="media"><img class="img" src="{{item.status === 1 ? item.serviceIcon : item.grayWechatIcon}}" alt=""></div>
+            <div class="title">{{item.serviceName}}</div>
+          </script>
+        </wx-open-launch-weapp>
+      </div>
+    </template>
+    <template v-else>
+      <div class="service-item" @click="onClick">
+        <img class="service-item-img" :src="item.status === 1 ? item.serviceIcon : item.grayWechatIcon" alt="" />
+        <div class="service-item-title">{{ item.serviceName }}</div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import { setLocalforage, interceptorUrl } from '@/utils'
+import { mapGetters } from 'vuex'
+
+export default {
+  props: {
+    service: {
+      type: Object,
+      default: () => ({}),
+    },
+    wxReady: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'org']),
+    item() {
+      const userInfo = this.userInfo
+      const v = { ...this.service }
+      // 微信跳转小程序标签
+      if (Number(v.linkUrlType) === 2) {
+        v.weappGhId = v.jumpAddress.split(';')[0]
+        v.weappAppId = v.jumpAddress.split(';')[1]
+        v.weappPath = v.jumpAddress.split(';')[2]
+        if (userInfo.userToken && v.weappPath) {
+          v.weappPath = interceptorUrl(v.weappPath, { userToken: userInfo.userToken })
+        }
+      }
+      // 文本省略(兼容微信跳转小程序标签文本css省略不起作用)
+      v.serviceName = v.serviceName.length > 6 ? `${v.serviceName.slice(0, 5)}...` : v.serviceName
+
+      return v
+    },
+  },
+  methods: {
+    // TODO: 服务跳转业务
+    onClick() {
+      this.$emit('click', this.item)
+      this.externalLink(this.item)
+    },
+    /*
+     * 服务跳转业务
+     * @description
+     * userInfo.userType(用户类型) 0(普通用户(有企业信息的就称作白名单)), -1(游客), 2被禁用的白名单
+     * lowPower 是否低权限 0(游客不可操作), 1(游客可操作), 2(不需要登录)
+     * serviceLevel 服务级别 1-平台级;2-运营中心级;3-项目级
+     * isLinked 是否需要拼接参数 0-不需要;1-需要
+     * status 服务状态 0-下线;1-上线;2-灰度
+     * centrePower(中心权限) 1-是 0-否
+     * linkUrlType 跳转类型 0-内链,1-外链,2-小程序
+     * permissionControlType 权限控制类型 1-使用服务权限,2-使用身份权限
+     * enabled 权限控制类型 是否可用 1-可用 0-不可用
+     */
+    externalLink({
+      id,
+      jumpAddress: url,
+      serviceName: name,
+      linkUrlType,
+      lowPower,
+      status,
+      graySkipUrl,
+      serviceLevel,
+      permissionControlType,
+      enabled,
+    }) {
+      // console.log(url, lowPower, serviceLevel, 'url', 'lowPower', 'serviceLevel')
+
+      const userToken = this.userInfo.userToken
+      // 灰度链接
+      if (status === 2 && graySkipUrl) {
+        window.location.href = `${graySkipUrl}`
+        return false
+      }
+      // 是否登录
+      if (!this.userInfo.isLogin && (lowPower === 1 || lowPower === 0)) {
+        setLocalforage('url', url)
+        setLocalforage('lowPower', lowPower)
+        return this.$router.push('/login')
+      } else {
+        setLocalforage('url', null)
+        setLocalforage('lowPower', null)
+      }
+
+      setLocalforage('accType', serviceLevel)
+      // 用户已经登录,跳转子服务,小程序逻辑
+      if (linkUrlType === 2) {
+        this.$dialog.alert({ title: '提示', message: '抱歉,暂时不能跳转小程序' })
+        return false
+      }
+      // 用户已经登录,跳转子服务,网页逻辑
+      const jumpH5Logic = () => {
+        const openLink = () => {
+          if (this.userInfo.isLogin) {
+            window.location.href = interceptorUrl(url, { userToken })
+          } else {
+            window.location.href = interceptorUrl(url)
+          }
+        }
+        // 权限控制为用户身份类型
+        if (permissionControlType === 2) {
+          if (!enabled) {
+            this.$dialog.alert({ title: '提示', message: '抱歉,您没有权限使用本服务' })
+            return false
+          }
+        }
+        // 针对运营中心级别项目,用户拥有运营中心权限
+        if (this.userInfo.centrePower && serviceLevel === 2 && url) {
+          openLink()
+          return false
+        }
+        // 针对运营中心级别项目,用户没有运营中心权限
+        if (!this.userInfo.centrePower && serviceLevel === 2) {
+          return this.$dialog.alert({
+            title: '提示',
+            message: '你没有运营中心操作权限',
+          })
+        }
+        // 操作权限,用户必须登录且是白名单用户才允许访问
+        if (lowPower === 0 && this.userInfo.userType !== 0) {
+          this.$dialog.alert({ title: '提示', message: '你没有操作权限' })
+          return
+        }
+
+        if (this.userInfo.userType === 0 || (this.userInfo.userType === -1 && lowPower === 1) || lowPower === 2) {
+          if (name === '我的房屋') {
+            return this.$router.push('/service/myHouse/index')
+          }
+          if (name === '住户认证') {
+            return this.$router.push('/service/houseHold/index')
+          }
+          if (url) {
+            openLink()
+          }
+        } else {
+          this.$dialog.alert({
+            title: '提示',
+            message: '你没有操作权限',
+          })
+        }
+      }
+      // 权限控制为用户身份类型
+      if (permissionControlType === 2) {
+        if (enabled) {
+          jumpH5Logic()
+        } else {
+          this.$dialog.alert({ title: '提示', message: '抱歉,您没有权限使用本服务', onHide() {} })
+        }
+        return false
+      }
+      // 权限控制为服务权限(默认)
+      jumpH5Logic()
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.service-item {
+  width: 100%;
+  background: #fff;
+  text-align: center;
+  &-perch {
+    background: #eee;
+    height: 88px;
+  }
+  &-img {
+    width: 50px;
+    height: 50px;
+    object-fit: cover;
+  }
+  &-title {
+    font-size: 14px;
+    height: 20px;
+    line-height: 20px;
+    padding-bottom: 16px;
+    color: #333;
+    text-align: center;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+}
+</style>

+ 47 - 0
src/components/SpainList/index.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="spain-list">
+    <template v-if="isLoading">
+      <van-loading class="spain-list-loading" size="24px" align="center">加载中...</van-loading>
+    </template>
+    <template v-else>
+      <template v-if="isEmpty">
+        <slot />
+      </template>
+      <template v-else>
+        <slot name="empty">
+          <empty />
+        </slot>
+      </template>
+    </template>
+  </div>
+</template>
+
+<script>
+import Empty from '@/components/Empty'
+
+export default {
+  name: 'SpainList',
+  components: { Empty },
+  props: {
+    isLoading: {
+      type: Boolean,
+      default: false,
+    },
+    isEmpty: {
+      type: [Boolean, String, Number],
+      default: false,
+    },
+  },
+  data() {
+    return {
+      list: [],
+    }
+  },
+}
+</script>
+
+<style>
+.spain-list-loading {
+  padding-top: 10vh;
+}
+</style>

+ 93 - 0
src/components/Tabbar/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <van-tabbar :value="active" active-color="#C23C30" fixed style="border-top: 1px solid #EEEEEE;">
+    <van-tabbar-item v-for="(item, index) in tabList" :key="index" @click.native="onTabClicked(index)">
+      <template v-slot:icon>
+        <img :src="active === index ? item.active : item.inactive" />
+      </template>
+      <span :class="[active === index ? 'active' : '']">{{ item.title }}</span>
+    </van-tabbar-item>
+  </van-tabbar>
+</template>
+
+<script>
+import usePage from '@/mixins/usePage'
+import { getServicePath } from '@/utils/constans'
+import { setLocalforage } from '@/utils'
+
+export default {
+  name: 'Tabbar',
+  mixins: [usePage],
+  data() {
+    return {
+      active: 0,
+      tabList: [
+        {
+          title: '首页',
+          active: require('@/assets/images/tabbar/home_on.png'),
+          inactive: require('@/assets/images/tabbar/home.png'),
+          path: '/home',
+        },
+        // {
+        //   title: '服务',
+        //   active: require('@/assets/images/tabbar/service_on.png'),
+        //   inactive: require('@/assets/images/tabbar/service.png'),
+        //   path: '/service',
+        // },
+        {
+          title: '订单',
+          active: require('@/assets/images/tabbar/order_on.png'),
+          inactive: require('@/assets/images/tabbar/order.png'),
+          path: '/orders',
+        },
+        // {
+        //   title: '我的',
+        //   active: require('@/assets/images/tabbar/mine_on.png'),
+        //   inactive: require('@/assets/images/tabbar/mine.png'),
+        //   path: '/mine',
+        // },
+      ],
+    }
+  },
+
+  watch: {
+    $route: {
+      handler({ path }) {
+        const index = this.tabList.findIndex((v) => v.path.includes(path))
+        if (path.includes('/orders')) {
+          return
+        }
+        this.active = ~index ? index : 0
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    onTabClicked(index) {
+      const { path } = this.tabList[index]
+      if (path.includes('/orders')) {
+        this.jumpOrderPage()
+      } else {
+        this.$router.replace({
+          path,
+        })
+      }
+    },
+    jumpOrderPage() {
+      // const FLASHDELIVER_ORDER_URL = getServicePath('flashdeliver_order')
+      // const ORDERMEAL_ORDER_URL = getServicePath('ordermeal_order')
+
+      const orerUrl = this.usePage_createURL({
+        path: getServicePath('ordermeal_order'),
+      })
+
+      this.usePage_openURL({ link: orerUrl, needLogin: true }) // 订餐订单地址
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.active {
+  color: var(--brand-color);
+}
+</style>

+ 51 - 0
src/components/brand.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="header">
+    <img class="logo" :src="logo" alt="" />
+    <div class="title">
+      {{ title }}
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      logo: require('@/assets/logo.png'),
+      title: '珠江啤酒食堂饭卡充值',
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.header {
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.logo {
+  width: 110px;
+  height: 110px;
+  border-radius: 50%;
+  overflow: hidden;
+}
+.title {
+  font-weight: bold;
+  color: #333333;
+  font-size: 21px;
+  margin-bottom: 46px;
+}
+
+@media screen and (min-width: 750px) {
+  .logo {
+    width: 210px;
+    height: 210px;
+  }
+  .title {
+    font-size: 38px;
+    margin-bottom: 37px;
+  }
+}
+</style>

+ 31 - 0
src/main.js

@@ -0,0 +1,31 @@
+import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+// Vant
+import Vant, { Lazyload } from 'vant'
+import 'vant/lib/index.less'
+// Vue.prototype.$toast = Toast
+// Vue.prototype.$dialog = Dialog
+
+Vue.use(Lazyload)
+Vue.use(Vant)
+
+//
+import './permission'
+// 样式
+import variables from './styles/variable.less'
+import './styles/index.less'
+
+Vue.config.productionTip = false
+
+Vue.prototype.$theme = variables
+
+// 忽略微信开放标签
+Vue.config.ignoredElements = [/^wx-/]
+
+new Vue({
+  router,
+  store,
+  render: (h) => h(App),
+}).$mount('#app')

+ 49 - 0
src/permission.js

@@ -0,0 +1,49 @@
+import router from '@/router'
+import store from '@/store'
+import { getURLParameters, getEnv } from '@/utils'
+
+window.appLoadedFlag = false // 应用窗口加载标记
+
+router.beforeEach(async (to, from, next) => {
+  console.log('[route]', from.path, to.path)
+  // add route title
+  const title = to.meta && to.meta.title
+  if (title) {
+    document.title = title
+  }
+  // 应用窗口加载
+
+  if (!window.appLoadedFlag) {
+    window.appLoadedFlag = true
+    console.info('app onLaunch')
+    console.info('app version', process.env.APP_VERSION)
+    console.info('app URL', window.location.href)
+    console.info('app URL Parameters', getURLParameters())
+    console.info(
+      '[用户信息] 设备信息: 是否安卓',
+      getEnv().isAndroid,
+      '是否IOS',
+      getEnv().isIOS,
+      '是否微信内打开',
+      getEnv().isInWeixinApp,
+      '是否小程序内打开',
+      getEnv().isInMiniProgram,
+    )
+    // 客户端平台
+    setPlatform()
+  }
+
+  next()
+})
+
+function setPlatform() {
+  if (getEnv().isAndroid || getEnv().isIOS) {
+    store.commit('setPlatform', 'H5')
+  }
+
+  if (getEnv().isInWeixinApp) {
+    store.commit('setPlatform', 'WEIXIN')
+  }
+
+  console.log('客户端平台', store.state.platform)
+}

+ 80 - 0
src/router/index.js

@@ -0,0 +1,80 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+Vue.use(VueRouter)
+
+// 解决路由在 push/replace 了相同地址报错的问题
+const originalPush = VueRouter.prototype.push
+VueRouter.prototype.push = function push(location) {
+  return originalPush.call(this, location).catch((err) => err)
+}
+const originalReplace = VueRouter.prototype.replace
+VueRouter.prototype.replace = function replace(location) {
+  return originalReplace.call(this, location).catch((err) => err)
+}
+
+const routes = [
+  {
+    path: '/',
+    redirect: '/home',
+  },
+  {
+    path: '/home',
+    name: 'homeIndex',
+    component: () => import(/* webpackChunkName: "manifest" */ '@/views/home/index.vue'),
+    meta: {
+      keepAlive: false,
+      title: '首页',
+    },
+  },
+  {
+    path: '/recharge',
+    name: 'rechargeIndex',
+    component: () => import(/* webpackChunkName: "manifest" */ '@/views/recharge/index.vue'),
+    meta: {
+      keepAlive: false,
+      title: '充值',
+    },
+  },
+  {
+    path: '/paycode',
+    name: 'paycodeIndex',
+    component: () => import(/* webpackChunkName: "manifest" */ '@/views/paycode/index.vue'),
+    meta: {
+      keepAlive: false,
+      title: '充值',
+    },
+  },
+  {
+    path: '/payresult',
+    name: 'payresultIndex',
+    component: () => import(/* webpackChunkName: "manifest" */ '@/views/payresult/index.vue'),
+    meta: {
+      keepAlive: false,
+      title: '充值',
+    },
+  },
+  // errorPage
+  {
+    path: '/404',
+    component: () => import(/* webpackChunkName: "manifest" */ '@/views/error-page/404.vue'),
+    meta: {
+      title: '404',
+    },
+  },
+  // 404 Not found
+  { path: '*', redirect: '/404' },
+]
+
+const router = new VueRouter({
+  routes,
+  scrollBehavior(to, from, savedPosition) {
+    if (savedPosition) {
+      return savedPosition
+    } else {
+      return { x: 0, y: 0 }
+    }
+  },
+})
+
+export default router

+ 31 - 0
src/store/index.js

@@ -0,0 +1,31 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import { setLocalforage } from '@/utils'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {
+    userInfo: {},
+    platform: 'PC', // PC, WEIXIN, H5
+  },
+  mutations: {
+    login(state, data) {
+      // state.userInfo = data
+      // setLocalforage('userInfo', data)
+    },
+    logout(state, data) {
+      // state.userInfo = {}
+      // setLocalforage('userInfo', {})
+    },
+    setPid(state, payload) {
+      state.pid = payload
+      setLocalforage('pid', payload)
+    },
+    setPlatform(state, payload) {
+      state.platform = payload
+    },
+  },
+  actions: {},
+})

+ 36 - 0
src/styles/index.less

@@ -0,0 +1,36 @@
+@import '~@/styles/variable.less';
+
+:root {
+  --brand-color: @brand-color;
+}
+
+.text-brand-color {
+  color: @brand-color;
+  color: var(--brand-color);
+}
+.mr5 {
+  margin-right: 5px;
+}
+.fwbold {
+  font-weight: bold;
+}
+.has-tabbar {
+  padding-bottom: calc(50px + constant(safe-area-inset-bottom));
+  padding-bottom: calc(50px + env(safe-area-inset-bottom));
+}
+
+@media screen and (min-width: 750px) {
+  body {
+    box-sizing: border-box;
+    font-size: 28px;
+    background: #fff;
+  }
+
+  .container {
+    box-sizing: border-box;
+    width: 750px;
+    margin: 0 auto;
+    padding: 10vh 80px 0;
+    color: #333333;
+  }
+}

+ 63 - 0
src/styles/variable.less

@@ -0,0 +1,63 @@
+// Color Palette
+@black: #000;
+@white: #fff;
+@gray-1: #f7f8fa;
+@gray-2: #f2f3f5;
+@gray-3: #ebedf0;
+@gray-4: #dcdee0;
+@gray-5: #c8c9cc;
+@gray-6: #999;
+@gray-7: #666;
+@gray-8: #333;
+@red: #c40008;
+@blue: #1989fa;
+@orange: #ff976a;
+@orange-dark: #ed6a0c;
+@orange-light: #fffbe8;
+@green: #07c160;
+
+// Gradient Colors
+@gradient-red: linear-gradient(to right, #ff6034, #ee0a24);
+@gradient-orange: linear-gradient(to right, #ffd01e, #ff8917);
+
+// Component Colors
+@text-color: @gray-8;
+@active-color: @gray-2;
+@active-opacity: 0.7;
+@disabled-opacity: 0.5;
+@background-color: @gray-1;
+@background-color-light: #fafafa;
+@text-link-color: #576b95;
+
+// Padding
+@padding-base: 4px;
+@padding-xs: @padding-base * 2;
+@padding-sm: @padding-base * 3;
+@padding-md: @padding-base * 4;
+@padding-lg: @padding-base * 6;
+@padding-xl: @padding-base * 8;
+
+// Font
+@font-size-xs: 10px;
+@font-size-sm: 12px;
+@font-size-md: 14px;
+@font-size-lg: 16px;
+@font-size-xlg: 18px;
+
+// border
+@border-color: #ededed;
+// 自定义UI主题
+@border-radius: 8px;
+// 主色调
+@brand-color: #0aa9f4;
+@brand-border-radius: 8px;
+
+@button-primary-background-color: @brand-color;
+@button-primary-border-color: @brand-color;
+@button-border-radius: 4px;
+// Dialog 弹出框
+@dialog-confirm-button-text-color: @brand-color;
+
+:export {
+  brandcolor: @brand-color;
+}

+ 38 - 0
src/utils/constans.js

@@ -0,0 +1,38 @@
+import { getAPI } from '@/utils'
+
+/**
+ * 服务地址
+ * @param {*} code
+ * @returns
+ */
+export function getServicePath(code) {
+  // /data/wwwroot/default/merge/wx/ordermealwx
+  const servicePath = {
+    selfpay: '/pcm/selfpay',
+    flashdeliver: '/wx/flashdeliver',
+    flashdeliver_order: '/wx/flashdeliver/#/order',
+    ordermeal: '/wx/ordermealwx?moduleCode=ordermeal',
+    ordermeal_order: '/wx/ordermealwx?moduleCode=ordermeal',
+    ordermeal_myCollect: '/wx/ordermealwx?moduleCode=myCollect',
+  }
+  if (!servicePath[code]) {
+    throw new Error('错误的服务链接')
+  }
+  return `${getAPI()}${servicePath[code]}`
+}
+
+/**
+ * 返回聚合码地址
+ * @param {*} url
+ * @returns
+ */
+export function getPayAPI(url) {
+  // 开发,测试环境下
+  if (process.env.VUE_APP_ENV !== 'prod') {
+    const patten = /^(https?:)?\/\/qr.95516.com/
+    const host = 'http://payment-uat.cs.cmburl.cn'
+    return url.replace(patten, host)
+  }
+
+  return url
+}

+ 183 - 0
src/utils/index.js

@@ -0,0 +1,183 @@
+export { isDef, isSrc } from './validate'
+/**
+ * 获取链接某个参数 search
+ * @param {String} name 参数名称
+ * @returns {String} 返回参数值
+ */
+export function getQueryString(name) {
+  let reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i')
+  let r = window.location.search.substr(1).match(reg)
+  // eslint-disable-next-line eqeqeq, no-eq-null
+  if (r != null) {
+    return decodeURIComponent(r[2])
+  }
+  return null
+}
+
+/**
+ * 获取网址参数 search 和 hash
+ * @returns {Object} 返回包含当前URL参数的对象
+ *
+ */
+export function getURLParameters() {
+  const reg = /([^?=&]+)(=([^&]*))/g
+  const searchParamList = window.location.search.match(reg)
+  const hashParamList = window.location.hash.match(reg)
+  const obj = {}
+
+  searchParamList &&
+    searchParamList.forEach((v) => {
+      obj[v.slice(0, v.indexOf('='))] = decodeURIComponent(v.slice(v.indexOf('=') + 1))
+    })
+
+  hashParamList &&
+    hashParamList.forEach((v) => {
+      obj[v.slice(0, v.indexOf('='))] = decodeURIComponent(v.slice(v.indexOf('=') + 1))
+    })
+
+  return obj
+}
+/**
+ * getEnv
+ * @returns {} 环境信息
+ */
+export function getEnv() {
+  const ua = navigator.userAgent.toLowerCase() // 一律小写
+  const isAndroid = /android/i.test(ua)
+  const isIOS = /iphone|ipad|ipod|ios/i.test(ua)
+  const isInMiniProgram = /miniProgram/i.test(ua) // 微信小程序内
+  const isInWeixinApp = /micromessenger/i.test(ua) // 微信内
+  const isInWeChatDevTools = /wechatdevtools/i.test(ua) // 微信开发者工具内
+
+  return {
+    isAndroid,
+    isIOS,
+    isInMiniProgram,
+    isInWeixinApp,
+    isInWeChatDevTools,
+  }
+}
+
+/**
+ * 接口前缀替换为当前部署站点域名
+ * @param {*} url
+ * @param {*} host
+ * @return {String}
+ */
+export function replaceHost(str, host = process.env.VUE_APP_BASE_HOST) {
+  let url = str
+  if (process.env.NODE_ENV === 'production') {
+    const patten = new RegExp(host)
+    url = str.replace(patten, location.host)
+  }
+
+  return url
+}
+/**
+ *
+ * @param {*} code api, host, origin
+ * @returns
+ */
+export function getAPI(code = 'api') {
+  const api = replaceHost(process.env.VUE_APP_BASE_API)
+
+  if (code === 'origin') {
+    return replaceHost(process.env.VUE_APP_BASE_ORIGIN)
+  }
+
+  if (code === 'host') {
+    return replaceHost(process.env.VUE_APP_BASE_HOST)
+  }
+
+  return api
+}
+/**
+ * idDefString
+ * @param {*} str
+ */
+export function idDefString(str) {
+  return str !== 'undefined' && str !== 'null'
+}
+/**
+ * 获取服务之窗的缓存
+ * @param {*} key
+ * @param {*} def
+ * @returns
+ */
+export function getLocalforage(key, def) {
+  const str = localStorage.getItem(`localforage/${key}`)
+  if (str) {
+    try {
+      return idDefString(JSON.parse(str)) ? JSON.parse(str) : ''
+    } catch (e) {
+      return str || null
+    }
+  } else {
+    // eslint-disable-next-line eqeqeq
+    return def != undefined ? def : null
+  }
+}
+
+/**
+ * 设置服务之窗的缓存
+ * @param {*} key
+ * @param {*} data
+ * @returns
+ */
+export function setLocalforage(key, data) {
+  localStorage.setItem(`localforage/${key}`, JSON.stringify(data))
+}
+
+/**
+ * 生成版本号(时间戳 按5分钟)
+ * @returns {String}
+ */
+export function getTimeStampVersion() {
+  const now = new Date()
+  const year = now.getFullYear()
+  const month = now.getMonth() + 1
+  const date = now.getDate()
+  const hour = now.getHours()
+  const minutes = now.getMinutes()
+  const padZero = (n) => String(n).padStart(2, '0')
+  const base = 5
+
+  return `${year}${padZero(month)}${padZero(date)}${padZero(hour)}${padZero(base * Math.floor(minutes / base))}`
+}
+
+/**
+ * 给url 追加参数
+ * @param {*} url
+ * @param {Object} query
+ * @description 多加了个版本号_v,避免缓存
+ * @returns {String}
+ */
+export function interceptorUrl(url, query = {}) {
+  const search = url.split('#')[0]
+  const hash = url.split('#')[1]
+  const params = {
+    ...query,
+    _v: getTimeStampVersion(),
+  }
+  // eslint-disable-next-line no-implicit-coercion
+  let link = `${search}${~search.indexOf('?') ? '&' : '?'}`
+
+  link += Object.keys(params)
+    .map((key) => key + '=' + encodeURIComponent(params[key]))
+    .join('&')
+
+  if (hash) {
+    link += `#${hash}`
+  }
+
+  return link
+}
+
+/**
+ * 验证 - 是否是手机号
+ * @param {String} val
+ */
+export function isPhone(value) {
+  const reg = /^1[0-9]{10}$/
+  return reg.test(value)
+}

+ 106 - 0
src/utils/lodash.js

@@ -0,0 +1,106 @@
+/**
+ *Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。
+ */
+
+/**
+ * 函数节流
+ * @param {*} fn 事件回调
+ * @param {*} interval 时间间隔的阈值
+ */
+export const throttle = function(fn, interval) {
+  let last = 0
+  return function() {
+    // eslint-disable-next-line no-invalid-this
+    const context = this
+    const args = arguments
+    const now = Number(new Date())
+
+    if (now - last >= interval) {
+      last = now
+      fn.apply(context, args)
+    }
+  }
+}
+
+/**
+ * 函数防抖
+ * @param {callback} fn 事件回调
+ * @param {number} delay 每次推迟执行的等待时间
+ */
+export const debounce = function(fn, delay) {
+  let last = 0
+  let timer = null
+  return function() {
+    // eslint-disable-next-line no-invalid-this
+    const context = this
+    const args = arguments
+    const now = Number(new Date())
+
+    if (now - last < delay) {
+      clearTimeout(timer)
+      timer = setTimeout(function() {
+        last = now
+        fn.apply(context, args)
+      }, delay)
+    } else {
+      last = now
+      fn.apply(context, args)
+    }
+  }
+}
+
+/**
+ * 浅拷贝
+ * @param {Object} source
+ */
+export function shallowClone(source) {
+  if (!source && typeof source !== 'object') {
+    throw new Error('error arguments', 'shallowClone')
+  }
+  return { ...source }
+}
+
+/**
+ * 深拷贝
+ * @param {Object} source
+ */
+export function deepClone(source) {
+  if (!source && typeof source !== 'object') {
+    throw new Error('error arguments', 'deepClone')
+  }
+  const targetObj = source.constructor === Array ? [] : {}
+  Object.keys(source).forEach((keys) => {
+    if (source[keys] && typeof source[keys] === 'object') {
+      targetObj[keys] = deepClone(source[keys])
+    } else {
+      targetObj[keys] = source[keys]
+    }
+  })
+  return targetObj
+}
+
+/**
+ * 生成一个 UUID
+ */
+export const guid = function() {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+    let r = (Math.random() * 16) | 0
+    let v = c === 'x' ? r : (r & 0x3) | 0x8
+    return v.toString(16)
+  })
+}
+
+/**
+ * 数组转map
+ * @param {Array} array
+ * @param {String} array
+ */
+export const arrayToMap = function(array, key) {
+  const map = {}
+  if (array.length) {
+    array.forEach((item) => {
+      map[item[key]] = item
+    })
+  }
+  return map
+}

+ 100 - 0
src/utils/request.js

@@ -0,0 +1,100 @@
+import axios from 'axios'
+import { Toast } from 'vant'
+import store from '@/store'
+import router from '@/router'
+import { getAPI } from '@/utils'
+
+// create an axios instance
+const instance = axios.create({
+  baseURL: getAPI(),
+  timeout: 5000,
+  withCredentials: false,
+})
+
+// 设置post请求头
+// instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
+
+// 请求拦截器
+// instance.interceptors.request.use((request) => {
+//   store.state.userInfo.userToken && (request.headers.userToken = store.state.userInfo.userToken)
+//   return request
+// })
+
+// 响应拦截器
+instance.interceptors.response.use(
+  (response) => {
+    const res = response.data || {}
+    if (Number(res.code) === 200) {
+      return Promise.resolve(res)
+    } else {
+      errorResultMessage(res)
+      return Promise.reject(res)
+    }
+  },
+  (error) => {
+    console.error(error)
+    const { response } = error || {}
+    if (response) {
+      errorHttpMessage(response)
+    } else {
+      Toast({
+        message: '当前网络不可用,请检查你的网络设置', // 网络出错
+        duration: 5 * 1000,
+      })
+    }
+
+    return Promise.reject(error)
+  },
+)
+
+/**
+ * 业务错误
+ */
+function errorResultMessage(res) {
+  const { code, msg } = res
+  if (Number(code) === 401) {
+    Toast.clear()
+    store.commit('logout')
+    toLogin()
+  } else {
+    Toast({
+      message: msg || 'Error',
+    })
+  }
+}
+
+/**
+ * HTTP错误
+ */
+function errorHttpMessage(res) {
+  const { status } = res
+  let message = ''
+
+  if (status) {
+    switch (Number(status)) {
+      case 403:
+        message = `${status} 网络请求被拒绝`
+        break
+      case 404:
+        message = `${status} 网络请求不存在`
+        break
+      case 500:
+        message = `${status} 服务器内部错误`
+        break
+    }
+  }
+
+  Toast({
+    message,
+    duration: 5 * 1000,
+  })
+}
+
+/**
+ * 跳转登录页面
+ */
+function toLogin() {
+  console.log('跳转登录页')
+  router.replace('/login')
+}
+export default instance

+ 153 - 0
src/utils/validate.js

@@ -0,0 +1,153 @@
+// 数据校验函数
+
+/**
+ * 验证 - 是否有值
+ * @param {*} value
+ */
+export function isDef(value) {
+  return value !== undefined && value !== null
+}
+
+/**
+ * 验证 - 是否是对象
+ * @param {*} x
+ */
+export function isObj(x) {
+  const type = typeof x
+  return x !== null && (type === 'object' || type === 'function')
+}
+
+/**
+ * 验证 - 是否是字符串
+ * @param {string} str
+ */
+export function isString(str) {
+  if (typeof str === 'string' || str instanceof String) {
+    return true
+  }
+  return false
+}
+
+/**
+ * 验证 - 是否是数字
+ * @param {Number} value
+ */
+export function isNumber(value) {
+  return /^\d+$/.test(value)
+}
+
+/**
+ * 验证 - 是否是数组
+ * @param {Array} arg
+ */
+export function isArray(arg) {
+  if (typeof Array.isArray === 'undefined') {
+    return Object.prototype.toString.call(arg) === '[object Array]'
+  }
+  return Array.isArray(arg)
+}
+
+/**
+ * 验证 - 是否是对象{},非数组情况
+ * @param {Array} arg
+ */
+export function isObject(arg) {
+  return Object.prototype.toString.call(arg) === '[object Object]'
+}
+
+/**
+ * 验证 - 是否为空数据
+ * @param {*} obj
+ */
+export function isEmpty(obj) {
+  if (obj === null || obj === undefined) {
+    return true
+  }
+  if (isArray(obj)) {
+    return obj.length === 0
+  }
+  if (isString(obj)) {
+    return !`${obj}`.trim().length
+  }
+  if (JSON.stringify(obj) === '{}') {
+    return true
+  }
+  return false
+}
+/**
+ * 验证 - 是否是本地资源路径
+ * @param {String} src
+ */
+export function isSrc(src) {
+  const reg = /^(https?:)?\/\/.+$/i
+  if (reg.test(src)) {
+    return false
+  } else {
+    return true
+  }
+}
+
+/**
+ * 验证 - 是否是手机号
+ * @param {String} val
+ */
+export function isPhone(value) {
+  const reg = /^1[0-9]{10}$/
+  return reg.test(value)
+}
+/**
+ * 验证 - 是否是邮箱
+ * @param {String} val
+ */
+export function isEmail(email) {
+  const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+  return reg.test(email)
+}
+/**
+ * 验证 - 手机验证码
+ * @param {String} value
+ * @description 6位纯数字组合
+ */
+export function isVcode(value) {
+  const reg = /^[0-9]{6}$/
+  return reg.test(value)
+}
+
+/**
+ * 验证 - 图形验证码
+ * @param {String} value
+ * @description 4位大小写字母、数字组合
+ */
+export function isCaptchaCode(value) {
+  const reg = /^[0-9A-Za-z]{4}$/
+  return reg.test(value)
+}
+
+/**
+ * 验证 - 用户名
+ * @param {String} value
+ * @description 132位的数字、字母、下划线组合,不能以下划线、横线开头
+ */
+export function isUsername(value) {
+  const reg = /^(?!_)(?!-)\w{1,31}[a-zA-Z0-9]$/
+  return reg.test(value)
+}
+
+/**
+ * 验证 - 账户密码
+ * @param {String} value
+ * @description 8-25位大小写字母、数字或数字加字母的形式
+ */
+export function isPassword(value) {
+  const reg = /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,25}$/
+  return reg.test(value)
+}
+
+/**
+ * 验证 - 数据是否一致
+ * @param {String} value1
+ * @param {String} value2
+ */
+export function isSame(value1, value2) {
+  return Object.is(value1, value2)
+}

+ 3 - 0
src/views/error-page/401.vue

@@ -0,0 +1,3 @@
+<template>
+  <h2>401</h2>
+</template>

+ 3 - 0
src/views/error-page/404.vue

@@ -0,0 +1,3 @@
+<template>
+  <h2>404</h2>
+</template>

+ 157 - 0
src/views/home/index.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="container">
+    <brand />
+    <div class="field">
+      <div class="field-label">
+        请输入手机号:
+      </div>
+      <van-field v-model="userMobile" maxlength="11" type="number" placeholder="请输入手机号" />
+    </div>
+    <van-button class="btn-submit" block @click="onNext">下一步</van-button>
+    <!-- <div v-if="platform === 'H5'" class="toptips">
+      <span>支付宝支付请使用手机浏览器打开</span>
+      <img :src="icon_arrow" alt="" srcset="" />
+    </div> -->
+  </div>
+</template>
+
+<script>
+import API_INDEX from '@/apis/index'
+import { getURLParameters, isPhone } from '@/utils'
+import brand from '@/components/brand.vue'
+
+export default {
+  name: 'HomeIndex',
+  components: { brand },
+  data() {
+    return {
+      icon_arrow: require('@/assets/images/icon_arrow.png'),
+      logo: require('@/assets/logo.png'),
+      title: '珠江啤酒食堂饭卡充值',
+      userMobile: '',
+    }
+  },
+  computed: {
+    platform() {
+      return this.$store.state.platform
+    },
+  },
+  created() {
+    const { userMobile } = getURLParameters()
+
+    if (userMobile) {
+      this.userMobile = userMobile
+    }
+    this.getIndexServes()
+  },
+  methods: {
+    onNext() {
+      if (!isPhone(this.userMobile)) {
+        this.$toast('请输入有效手机号')
+        return
+      }
+      this.$router.push({
+        path: '/recharge',
+        query: {
+          userMobile: this.userMobile,
+        },
+      })
+    },
+    getIndexServes() {
+      // let params = {
+      //   category: 0,
+      //   publicId: this.pid,
+      //   openid: this.openId,
+      //   orgId: this.org.id,
+      //   subjectId: this.userInfo.subjectId || '',
+      // }
+      // API_INDEX.apiIndexServes(params).then((res) => {
+      //   const data = res.data || []
+      //   const len = 3
+      //   if (data.length > len) {
+      //     this.homeList = data.slice(0, len)
+      //   } else {
+      //     this.homeList = data
+      //   }
+      // })
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.field {
+  width: 100%;
+  margin-bottom: 24px;
+  &-label {
+    font-size: 17px;
+    margin-bottom: 10px;
+  }
+  /deep/ .van-field {
+    border-radius: 3px;
+    border: 1px solid #d4d4d4;
+  }
+}
+
+.btn-submit {
+  margin: 0 auto;
+  width: 140px;
+  height: 45px;
+  background: #0aa9f4;
+  border-radius: 3px;
+  font-size: 16px;
+  color: #fff;
+}
+
+// .toptips {
+//   box-sizing: border-box;
+//   position: fixed;
+//   padding: 0 20px;
+//   display: flex;
+//   align-items: center;
+//   top: 0;
+//   left: 0;
+//   width: 100%;
+//   height: 35px;
+//   background: #e9f8ff;
+//   color: #0aa9f4;
+//   font-size: 14px;
+// }
+
+@media screen and (max-width: 749px) {
+  .container {
+    padding: 10vh 20px;
+    box-sizing: border-box;
+    color: #333333;
+  }
+}
+
+@media screen and (min-width: 750px) {
+  .field {
+    margin-bottom: 24px;
+    &-label {
+      font-size: 30px;
+      margin-bottom: 10px;
+    }
+    /deep/ .van-field {
+      border-radius: 3px;
+      border: 1px solid #d4d4d4;
+      font-size: 26px;
+      line-height: 38px;
+      padding: 20px 22px;
+      overflow: hidden;
+      color: #333;
+    }
+  }
+
+  .btn-submit {
+    width: 252px;
+    height: 80px;
+    background: #0aa9f4;
+    border-radius: 5px;
+    font-size: 30px;
+    font-weight: 500;
+    color: #fff;
+  }
+}
+</style>

+ 176 - 0
src/views/paycode/index.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="container">
+    <template v-if="platform === 'PC'">
+      <div class="title">{{ title }}</div>
+      <div ref="qrCodeUrl" class="qrcode"></div>
+      <div class="tips">请使用微信 /支付宝扫一扫 扫码完成支付!</div>
+    </template>
+    <template v-else>
+      <div class="section">
+        <div class="section-title fw-bold">{{ title }}</div>
+        <div ref="qrCodeUrl" class="qrcode"></div>
+        <div class="back" @click="onBack">返回订餐</div>
+      </div>
+      <div class="section">
+        <div class="fw-bold mb5">
+          充值说明:
+        </div>
+        <div class="tips">
+          一、长按二维码识别并完成付款,充值完成。 <br />
+          二、长按保存图片并扫一扫付款: <br />
+          1、截图或长按保存收款码到手机; <br />
+          2、打开微信或支付宝的扫一扫 <br />3、进入图库 ,选择保存的收款码图片。 <br />
+          4、完成付款,充值完成。
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import QRCode from 'qrcodejs2'
+import { getServicePath } from '@/utils/constans'
+import { getLocalforage } from '@/utils'
+
+export default {
+  data() {
+    return {
+      title: '充值收款码',
+      code: '',
+    }
+  },
+  computed: {
+    platform() {
+      return this.$store.state.platform
+    },
+    qrcodeSize() {
+      return this.platform === 'PC' ? '345' : '170'
+    },
+  },
+  created() {
+    const { code } = this.$route.query
+    this.code = code
+    // this.code = code || 'https://qr.95516.com/03080000/1004/100421110117570981363009'
+    this.$nextTick().then(() => {
+      this.creatQrCode()
+    })
+  },
+  methods: {
+    onBack() {
+      // this.$router.back()
+      const dingcanUrl = getServicePath('ordermeal_order')
+      const userInfo = getLocalforage('userInfo') || {}
+      if (userInfo.userToken) {
+        const query = {
+          openId: getLocalforage('openId'),
+          state: getLocalforage('pid'),
+          orgId: getLocalforage('orgId'),
+          userId: userInfo.userId,
+          userToken: userInfo.userToken,
+        }
+
+        const params = Object.keys(query)
+          .map((key) => key + '=' + encodeURIComponent(query[key]))
+          .join('&')
+
+        location.href = `${dingcanUrl}&${params}`
+      } else {
+        this.$router.replace('/')
+      }
+    },
+    creatQrCode() {
+      console.log(this.code)
+      // eslint-disable-next-line no-unused-vars
+      const qrcode = new QRCode(this.$refs.qrCodeUrl, {
+        text: this.code,
+        width: this.qrcodeSize,
+        height: this.qrcodeSize,
+      })
+      // 解决安卓长按二维码不识别
+      const canvas = this.$refs.qrCodeUrl.children[0]
+      this.$refs.qrCodeUrl.innerHTML = ''
+      this.$refs.qrCodeUrl.appendChild(this.convertCanvasToImage(canvas))
+    },
+    convertCanvasToImage(canvas) {
+      let image = new Image()
+      image.src = canvas.toDataURL('image/png')
+      return image
+    },
+  },
+}
+</script>
+<style lang="scss" scoped>
+.fw-bold {
+  font-weight: bold;
+}
+
+@media screen and (max-width: 749px) {
+  .container {
+    box-sizing: border-box;
+    padding: 20px 15px;
+    color: #333333;
+  }
+
+  .section {
+    box-sizing: border-box;
+    padding: 20px;
+    background: #ffffff;
+    border-radius: 5px;
+    margin-bottom: 15px;
+
+    &-title {
+      text-align: center;
+      font-size: 18px;
+      margin-bottom: 20px;
+    }
+  }
+
+  .qrcode {
+    display: flex;
+    justify-content: center;
+    text-align: center;
+    margin-bottom: 30px;
+  }
+
+  .back {
+    text-align: center;
+    font-size: 14px;
+    color: #0061d2;
+  }
+
+  .tips {
+    font-size: 14px;
+    line-height: 1.6;
+  }
+  .mb5 {
+    margin-bottom: 5px;
+  }
+}
+
+@media screen and (min-width: 750px) {
+  .container {
+    box-sizing: border-box;
+    padding: 13vh 15px 0;
+    color: #333333;
+    text-align: center;
+  }
+
+  .title {
+    font-size: 36px;
+    font-weight: bold;
+    margin-bottom: 37px;
+  }
+
+  .qrcode {
+    display: flex;
+    justify-content: center;
+    text-align: center;
+    margin-bottom: 37px;
+  }
+
+  .tips {
+    font-size: 28px;
+    line-height: 1.6;
+  }
+}
+</style>

+ 103 - 0
src/views/payresult/index.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="container">
+    <h2>支付回调</h2>
+    <h2>
+      {{ $route.query }}
+    </h2>
+  </div>
+</template>
+<script>
+export default {
+  data() {
+    return {
+      code: '',
+      title: '充值收款码',
+    }
+  },
+  created() {
+    console.log(this.$route.query)
+  },
+  mounted() {},
+  methods: {
+    onBack() {
+      this.$router.back()
+    },
+  },
+}
+</script>
+<style lang="scss" scoped>
+.title {
+  
+}
+.result {
+  background: #fff;
+  border-radius: 16px;
+  padding: 40px;
+  margin-bottom: 40px;
+}
+
+.qrcode {
+  display: flex;
+  justify-content: center;
+  text-align: center;
+  margin-bottom: 20px;
+
+  &-h2 {
+    font-size: 32px;
+    color: #333;
+  }
+
+  &-p {
+    padding: 30px 0 40px;
+    font-weight: bold;
+    font-size: 60px;
+    color: #02a7f0;
+    animation: color 1s linear 0s infinite;
+  }
+}
+.pickupNum {
+  text-align: center;
+  margin-bottom: 20px;
+
+  &-h2 {
+    font-size: 32px;
+    color: #333;
+  }
+
+  &-p {
+    padding: 0 20px;
+    font-weight: bold;
+    font-size: 60px;
+    color: #02a7f0;
+    animation: color 1s linear 0s infinite;
+  }
+}
+
+@keyframes color {
+  0% {
+    color: #02a7f0;
+  }
+  100% {
+    color: #f59a23;
+  }
+}
+.p {
+  text-align: left;
+  font-size: 32px;
+  color: #333;
+  margin-bottom: 20px;
+}
+
+.btn {
+  display: block;
+  border: 1px solid #02a7f0;
+  color: #02a7f0;
+  background: #fff;
+  height: 78px;
+  line-height: 78px;
+  border-radius: 8px;
+  &:active {
+    opacity: 0.7;
+  }
+}
+</style>

+ 406 - 0
src/views/recharge/index.vue

@@ -0,0 +1,406 @@
+<template>
+  <div class="container">
+    <brand />
+    <div class="info">
+      <div class="info-title">你好,{{ userInfo.userName || userMobile }}</div>
+      <div v-if="showProject" class="info-title">公司:{{ project.name }}</div>
+    </div>
+    <div v-if="!showProject && platform !== 'PC'" class="section">
+      <div class="section-hd">
+        充值食堂
+      </div>
+      <div class="section-bd">
+        <van-field
+          readonly
+          clickable
+          name="picker"
+          :value="project.name"
+          placeholder="请选择食堂"
+          @click="showPicker = true"
+        />
+      </div>
+    </div>
+    <div v-if="!showProject && platform === 'PC'" class="section">
+      <div class="section-hd">
+        充值食堂
+      </div>
+      <div class="section-bd">
+        <div class="main-list">
+          <div v-for="(item, index) in projectList" :key="index" class="main-list-col">
+            <div
+              class="main-list-item fz28"
+              :class="[project.name === item.name ? 'active' : '']"
+              @click="onProjectConfirm(item)"
+            >
+              {{ item.name }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="section">
+      <div class="section-hd">
+        充值金额
+      </div>
+      <div class="section-field">
+        <div class="section-field-hd">
+          <span class="yuan">¥</span>
+        </div>
+        <van-field v-model="amount" class="recharge-field" type="number" placeholder="请输入充值金额" />
+      </div>
+      <div class="main-list">
+        <div v-for="(item, index) in list" :key="index" class="main-list-col">
+          <div class="main-list-item" @click="onDetail(index)">
+            {{ item.label }}
+          </div>
+        </div>
+      </div>
+    </div>
+    <van-button class="btn-invest" :disabled="!amount" type="primary" block color="#ff9400" @click="onInvest"
+      >充值</van-button
+    >
+
+    <van-popup v-model="showPicker" position="bottom">
+      <van-picker
+        show-toolbar
+        value-key="name"
+        :columns="projectList"
+        @confirm="onProjectConfirm"
+        @cancel="showPicker = false"
+      />
+    </van-popup>
+  </div>
+</template>
+<script>
+import API_INDEX from '@/apis/index'
+import brand from '@/components/brand.vue'
+import { getPayAPI } from '@/utils/constans'
+
+export default {
+  name: 'WallerInvest',
+  components: { brand },
+  data() {
+    return {
+      userMobile: '',
+      userInfo: '',
+      list: [
+        { label: '20', value: '20' },
+        { label: '50', value: '50' },
+        { label: '100', value: '100' },
+        { label: '200', value: '200' },
+      ],
+      show: false,
+      showPicker: false,
+      showProject: false,
+      projectList: [],
+      project: {
+        name: '',
+        orgId: '',
+      },
+      amount: '',
+    }
+  },
+  computed: {
+    platform() {
+      return this.$store.state.platform
+    },
+  },
+  created() {
+    this.userMobile = this.$route.query.userMobile
+    this.getUserInfo()
+    this.getProjectList()
+  },
+  methods: {
+    getUserInfo() {
+      API_INDEX.queryUserInfoByPhone(this.userMobile).then((res) => {
+        this.userInfo = res.data || {}
+        this.project = {
+          name: this.userInfo.orgName,
+          orgId: this.userInfo.orgId,
+        }
+        if (this.project.name) {
+          this.showProject = true
+        }
+      })
+    },
+    getProjectList() {
+      API_INDEX.getOrgInfo().then((res) => {
+        this.projectList = res.data || []
+      })
+    },
+    onProjectConfirm(data) {
+      this.project = data
+      this.showPicker = false
+    },
+    onProjectCancel() {
+      this.showPicker = false
+    },
+    onInput(value) {
+      console.log(value)
+    },
+    onDelete() {
+      console.log('onDelete')
+    },
+    onBlur() {
+      console.log('onBlur')
+      this.onInvest()
+    },
+    onInvest() {
+      console.log('onInvest')
+      if (!this.project.orgId) {
+        this.$toast('项目不能为空')
+        return
+      }
+      if (!this.amount) {
+        this.$toast('请选择充值金额')
+        return
+      }
+      const params = {
+        orgId: this.project.orgId,
+        subjectId: this.userInfo.subjectId || null,
+        amount: this.amount,
+        userMobile: this.userMobile,
+      }
+
+      if (this.platform === 'H5') {
+        params.type = 'ALIPAY'
+      }
+      if (this.platform === 'WEIXIN') {
+        params.type = 'WECHAT'
+      }
+
+      this.$toast.loading({
+        mask: true,
+        message: '加载中...',
+        duration: 0,
+      })
+      // this.$toast.clear()
+      // this.$router.push({
+      //   path: `/paycode`,
+      //   query: {
+      //     code: getPayAPI('https://payment-uat.cs.cmburl.cn/03080000/1004/100421110114201341531199'),
+      //   },
+      // })
+      // PC 聚合码
+      //       {
+      //     "code": 200,
+      //     "data": {
+      //         "code": 200,
+      //         "msg": "预支付成功",
+      //         "data": "https://qr.95516.com/03080000/1004/100421110117570981363009"
+      //     }
+      // }
+
+      API_INDEX.recharge(params).then((res) => {
+        this.$toast.clear()
+
+        // if (this.platform === 'PC' || this.platform === 'WEIXIN') {
+        // res.data = 'https://payment-uat.cs.cmburl.cn/03080000/1004/100421110114201341531199'
+        this.$router.push({
+          path: `/paycode`,
+          query: {
+            code: getPayAPI(res.data.data),
+          },
+        })
+        // } else {
+        //   console.log('充值成功')
+        // }
+      })
+    },
+    onBack() {
+      this.$router.back()
+    },
+    onDetail(index) {
+      const current = this.list[index]
+      this.amount = current.value
+    },
+    onAmoutClicked() {
+      if (this.amount || this.show) {
+        //
+      } else {
+        this.show = true
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.info {
+  box-sizing: border-box;
+  width: 100%;
+  margin-bottom: 10px;
+  padding: 10px 15px;
+  text-align: left;
+  background: #fff;
+}
+
+.mr15 {
+  margin-right: 15px;
+}
+
+.btn-invest--disabled {
+  background: #c4c4c4;
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+.section {
+  padding: 0 15px;
+  background: #ffffff;
+
+  &-hd {
+    display: flex;
+    align-items: center;
+    height: 40px;
+    font-size: 15px;
+    color: #333333;
+  }
+
+  .yuan {
+    font-size: 35px;
+  }
+
+  &-field {
+    display: flex;
+    align-items: center;
+    color: #333333;
+  }
+
+  .recharge-field {
+    font-size: 18px;
+    line-height: 34px;
+    padding: 15px 22px 5px;
+    overflow: hidden;
+    color: #333;
+  }
+}
+
+@media screen and (max-width: 749px) {
+  .container {
+    padding: 5vh 0;
+    box-sizing: border-box;
+    color: #333333;
+  }
+
+  .main-list {
+    display: flex;
+    flex-wrap: wrap;
+    padding: 20px 0;
+    overflow: hidden;
+    margin-left: -10px;
+    margin-right: -10px;
+
+    &-col {
+      box-sizing: border-box;
+      width: 25%;
+      margin-bottom: 10px;
+      padding-left: 10px;
+      padding-right: 10px;
+    }
+
+    &-item {
+      padding: 5px 0;
+      border-radius: 3px;
+      border: 1px solid #b5b5b5;
+      color: #333333;
+      font-size: 17px;
+      text-align: center;
+    }
+  }
+  .btn-invest {
+    margin-top: 30px;
+    margin-bottom: 30px;
+    margin-left: auto;
+    margin-right: auto;
+    width: 345px;
+    height: 45px;
+    border-radius: 5px;
+    font-size: 17px;
+    font-weight: bold;
+  }
+}
+@media screen and (min-width: 750px) {
+  .info {
+    font-size: 28px;
+    margin-bottom: 10px;
+    padding: 10px 0;
+    border-bottom: 1px solid #edeaea;
+  }
+  .section {
+    padding: 0;
+
+    &-hd {
+      height: 40px;
+      font-size: 28px;
+      margin-bottom: 10px;
+    }
+    .yuan {
+      font-size: 44px;
+    }
+
+    &-field {
+      display: flex;
+      align-items: center;
+      border-bottom: 1px solid #edeaea;
+
+      &-hd {
+        font-size: 36px;
+      }
+    }
+
+    .recharge-field {
+      font-size: 30px;
+      line-height: 34px;
+      padding: 15px 22px;
+      overflow: hidden;
+      color: #333;
+    }
+  }
+
+  .main-list {
+    display: flex;
+    flex-wrap: wrap;
+    padding: 20px 0;
+    overflow: hidden;
+    margin-left: -18px;
+    margin-right: -18px;
+
+    &-col {
+      box-sizing: border-box;
+      width: 25%;
+      margin-bottom: 10px;
+      padding-left: 18px;
+      padding-right: 18px;
+    }
+
+    &-item {
+      padding: 15px 0;
+      border-radius: 5px;
+      border: 1px solid #b5b5b5;
+      color: #333333;
+      font-size: 28px;
+      text-align: center;
+    }
+
+    .active {
+      color: #fff;
+      background: var(--brand-color);
+    }
+  }
+  .fz28 {
+    font-size: 20px;
+  }
+  .btn-invest {
+    margin-top: 30px;
+    margin-bottom: 30px;
+    margin-left: auto;
+    margin-right: auto;
+    width: 100%;
+    height: 80px;
+    line-height: 80px;
+    border-radius: 10px;
+    font-size: 30px;
+    font-weight: bold;
+  }
+}
+</style>

+ 89 - 0
vue.config.js

@@ -0,0 +1,89 @@
+const path = require('path')
+
+/**
+ * resolve
+ * @param {*} dir
+ * @returns
+ */
+function resolve(dir) {
+  return path.join(__dirname, dir)
+}
+
+/**
+ * 生成版本号(时间戳 按小时)
+ * @returns {String}
+ */
+function getTimeStampVersion() {
+  const now = new Date()
+  const year = now.getFullYear()
+  const month = now.getMonth() + 1
+  const date = now.getDate()
+  const hour = now.getHours()
+  const minutes = now.getMinutes()
+  const padZero = (n) => String(n).padStart(2, '0')
+
+  return `${year}${padZero(month)}${padZero(date)}${padZero(hour)}${padZero(minutes)}`
+}
+
+const appVersion = getTimeStampVersion()
+
+module.exports = {
+  publicPath: './',
+  lintOnSave: process.env.NODE_ENV === 'development',
+  productionSourceMap: false,
+  devServer: {
+    proxy: {
+      '/dev-api': {
+        target: 'http://your-proxy-path:3000',
+        secure: false,
+        ws: true,
+        pathRewrite: {
+          '^/dev-api': '',
+        },
+      },
+    },
+  },
+  css: {
+    loaderOptions: {
+      less: {
+        modifyVars: {
+          hack: `true; @import '~@/styles/variable.less';`,
+        },
+      }
+    },
+  },
+  chainWebpack(config) {
+    if (process.env.NODE_ENV === 'production') {
+      // js和css 使用版本号
+      config.output.filename(`js/[name].${appVersion}.js`).end()
+      config.output.chunkFilename(`js/[name].${appVersion}.js`).end()
+      config
+        .plugin('extract-css')
+        .tap((args) => {
+          args[0].filename = `css/[name].${appVersion}.css`
+          args[0].chunkFilename = `css/[name].${appVersion}.css`
+          return args
+        })
+        .end()
+    }
+    // 删除预加载
+    config.plugins.delete('preload')
+    config.plugins.delete('prefetch')
+    // HtmlWebpackPlugin
+    config
+      .plugin('html')
+      .tap((args) => {
+        args[0].title = process.env.VUE_APP_TITLE
+        return args
+      })
+      .end()
+    // DefinePlugin
+    config
+      .plugin('define')
+      .tap((args) => {
+        args[0]['process.env'].APP_VERSION = appVersion
+        return args
+      })
+      .end()
+  },
+}