Browse Source

Merge branch 'developer'

yuwenjun1997 2 years ago
parent
commit
d167af73df
59 changed files with 25874 additions and 211 deletions
  1. 4 4
      .env.development
  2. 2 0
      .gitignore
  3. 105 27
      apis/index.js
  4. 6 2
      components/LdmLogin/index.vue
  5. 44 0
      components/SimpleAMap/common/utils.js
  6. 160 0
      components/SimpleAMap/index.vue
  7. 15 0
      components/SimpleDeviceParams/index.vue
  8. 245 0
      components/SimpleDialog/index.vue
  9. 245 27
      components/SimpleLogin/index.vue
  10. 16 5
      components/SimplePagination/index.vue
  11. 272 0
      components/SimpleRadio/index.vue
  12. 98 0
      components/SimpleStep/index.vue
  13. 12 4
      components/SimpleTabs/index.vue
  14. 119 0
      components/SimpleUploadImage/index.vue
  15. 69 29
      layouts/app.vue
  16. 19 1
      nuxt.config.js
  17. 19488 1
      package-lock.json
  18. 3 0
      package.json
  19. 43 26
      pages/_template/app/approve/club/detail.vue
  20. 16 6
      pages/_template/app/approve/club/index.vue
  21. 15 6
      pages/_template/app/approve/device/index.vue
  22. 16 6
      pages/_template/app/approve/device/list.vue
  23. 15 7
      pages/_template/app/approve/index.vue
  24. 6 2
      pages/_template/app/approve/personnel/operate/detail.vue
  25. 16 7
      pages/_template/app/approve/personnel/operate/index.vue
  26. 4 1
      pages/_template/app/database/article.vue
  27. 10 3
      pages/_template/app/database/file.vue
  28. 7 2
      pages/_template/app/database/image.vue
  29. 7 2
      pages/_template/app/database/package.vue
  30. 7 2
      pages/_template/app/database/video.vue
  31. 6 2
      pages/_template/app/feedback/index.vue
  32. 527 0
      pages/_template/app/form/club-register.vue
  33. 596 0
      pages/_template/app/form/components/form-club-device.vue
  34. 806 0
      pages/_template/app/form/components/form-club-info.vue
  35. 221 0
      pages/_template/app/form/components/form-club-register.vue
  36. 271 0
      pages/_template/app/form/link-register.vue
  37. 61 37
      pages/_template/app/index.vue
  38. 399 0
      pages/_template/app/record/club/detail.vue
  39. 279 0
      pages/_template/app/record/club/edit.vue
  40. 362 0
      pages/_template/app/record/device/detail.vue
  41. 253 0
      pages/_template/app/record/device/edit.vue
  42. 321 0
      pages/_template/app/record/device/index.vue
  43. 116 0
      pages/_template/app/record/message.vue
  44. 138 0
      pages/_template/app/record/search.vue
  45. 72 0
      pages/_template/app/user/forget.vue
  46. 88 0
      pages/_template/app/user/login.vue
  47. 72 0
      pages/_template/app/user/register.vue
  48. 6 2
      pages/auth/index.vue
  49. 4 0
      plugins/element-ui.js
  50. BIN
      static/location.png
  51. 5 0
      static/map.config.js
  52. 5 0
      store/app.js
  53. 3 0
      store/getters.js
  54. 4 0
      store/user.js
  55. 28 0
      themes/themeMixin.scss
  56. 7 0
      themes/themeVariable.scss
  57. 60 0
      themes/variables/normal.scss
  58. 62 0
      themes/variables/ross.scss
  59. 18 0
      utils/validator.js

+ 4 - 4
.env.development

@@ -2,19 +2,19 @@
 EVN = 'development'
 
 # 网站地址
-LOCALHOSE = 'https://zp.caimei365.com'
+LOCALHOSE = 'https://zp-b.caimei365.com'
 # LOCALHOSE = 'http://192.168.2.81:8888'
-# LOCALHOSE = 'http://192.168.1.102:8888'
+# LOCALHOSE = 'http://192.168.2.92:8888'
 
 # 接口api地址
-BASE_URL = 'https://zplma.caimei365.com'
+BASE_URL = 'https://zplma-b.caimei365.com'
 # BASE_URL = 'http://192.168.2.68:8012'
 
 # 静态资源文件地址
 STATIC_URL = 'https://static.caimei365.com/www/authentic'
 
 # 采美网
-CIMEI_LOCAL = 'https://www.caimei365.com'
+CIMEI_LOCAL = 'https://www-b.caimei365.com'
 
 # 项目运行地址
 HOST = '192.168.2.92'

+ 2 - 0
.gitignore

@@ -88,3 +88,5 @@ sw.*
 
 # Vim swap files
 *.swp
+
+.env.*

+ 105 - 27
apis/index.js

@@ -4,62 +4,127 @@
  */
 const queryStringify = function (search = {}) {
   return Object.entries(search)
-    .reduce((t, v) => `${t}${v[0]}=${encodeURIComponent(v[1])}&`, Object.keys(search).length ? '?' : '')
+    .reduce(
+      (t, v) => `${t}${v[0]}=${encodeURIComponent(v[1])}&`,
+      Object.keys(search).length ? '?' : ''
+    )
     .replace(/&$/, '')
 }
 
 export default ($axios) => {
   // 订阅号用户登录
-  const customLogin = (data) => $axios.post('/wx/user/login/subscribe/verify/code', data)
+  const customLogin = (data) =>
+    $axios.post('/wx/user/login/subscribe/verify/code', data)
   // 订阅号用户绑定邀请码登录
-  const customLoginWithCode = (data) => $axios.post('/wx/user/login/subscribe/invitation/code', data)
+  const customLoginWithCode = (data) =>
+    $axios.post('/wx/user/login/subscribe/invitation/code', data)
   // 服务号微信授权登录
-  const wechatLogin = (data) => $axios.post('/wx/user/login/authorization', data)
+  const wechatLogin = (data) =>
+    $axios.post('/wx/user/login/authorization', data)
   // 服务号微信授权绑定邀请码登录
-  const wechatLoginWithCode = (data) => $axios.post('/wx/user/login/service/invitation/code', data)
+  const wechatLoginWithCode = (data) =>
+    $axios.post('/wx/user/login/service/invitation/code', data)
   // 获取jssdk配置参数
-  const initWxConfig = (params = {}) => $axios.get('/wx/sdk/config/data', { params })
+  const initWxConfig = (params = {}) =>
+    $axios.get('/wx/sdk/config/data', { params })
   // 发送验证码
-  const sendVerifyCode = (data = {}) => $axios.post('/wx/user/verify/code/send', data)
+  const sendVerifyCode = (data = {}) =>
+    $axios.post('/wx/user/login/verify/code/send', data)
   // 获取文章列表
-  const getArticleList = (params = {}) => $axios.get('/wx/data/article/list', { params })
+  const getArticleList = (params = {}) =>
+    $axios.get('/wx/data/article/list', { params })
   // 获取图片列表
-  const getImageList = (params = {}) => $axios.get('/wx/data/image/list', { params })
+  const getImageList = (params = {}) =>
+    $axios.get('/wx/data/image/list', { params })
   // 获取视频列表
-  const getVideoList = (params = {}) => $axios.get('/wx/data/video/list', { params })
+  const getVideoList = (params = {}) =>
+    $axios.get('/wx/data/video/list', { params })
   // 获取文件列表
-  const getFileList = (params = {}) => $axios.get('/wx/data/file/list', { params })
+  const getFileList = (params = {}) =>
+    $axios.get('/wx/data/file/list', { params })
   // 获取文章详情
-  const getArticleDetail = (params = {}) => $axios.get('/wx/data/article/form/data', { params })
+  const getArticleDetail = (params = {}) =>
+    $axios.get('/wx/data/article/form/data', { params })
   // 用户反馈
   const feedback = (data = {}) => $axios.post('/wx/data/feedback/submit', data)
   // 获取已认证机构列表
-  const getAuthClubList = (params = {}) => $axios.get('/wx/auth/club/list', { params })
+  const getAuthClubList = (params = {}) =>
+    $axios.get('/wx/auth/club/list', { params })
   // 获取已认证机构详情
-  const getAuthClubDetail = (params = {}) => $axios.get('/wx/auth/club/details', { params })
+  const getAuthClubDetail = (params = {}) =>
+    $axios.get('/wx/auth/club/details', { params })
   // 获取已认证商品分类
-  const getAuthProductCateList = (params = {}) => $axios.get('/wx/auth/product/type/list', { params })
+  const getAuthProductCateList = (params = {}) =>
+    $axios.get('/wx/auth/product/type/list', { params })
   // 获取已认证商品列表
-  const getAuthProductList = (params = {}) => $axios.get('/wx/auth/product/list', { params })
+  const getAuthProductList = (params = {}) =>
+    $axios.get('/wx/auth/product/list', { params })
   // 验证token是否到期
-  const checkToken = (data = {}) => $axios.post('/wx/user/token/check', data)
+  const checkToken = (data = {}) =>
+    $axios.post('/wx/user/login/token/check', data)
   // 下载文件
-  const downFile = (params = {}) => $axios.get('/download/file', { params })
+  const downFile = (params = {}) => $axios.get('/wx/download/file', { params })
   // 获取城市列表
-  const fetchCityList = (params = {}) => $axios.get('/address/select', { params })
+  const fetchCityList = (params = {}) =>
+    $axios.get('/wx/address/select', { params })
   // 获取城市列表
-  const fetchAllCityList = () => $axios.get('/address/select/all')
+  const fetchAllCityList = () => $axios.get('/wx/address/select/all')
   // 获取供应商信息
-  const fetchSupplierInfo = (params = {}) => $axios.get('/wx/auth/shop/info', { params })
+  const fetchSupplierInfo = (params = {}) =>
+    $axios.get('/wx/auth/shop/info', { params })
   // 获取医师列表
-  const fetchDoctorList = (params = {}) => $axios.get('/wx/auth/doctor/list', { params })
+  const fetchDoctorList = (params = {}) =>
+    $axios.get('/wx/auth/doctor/list', { params })
   // 获取医师详情
-  const fetchDoctorDetail = (params = {}) => $axios.get('/wx/auth/doctor/details', { params })
+  const fetchDoctorDetail = (params = {}) =>
+    $axios.get('/wx/auth/doctor/details', { params })
   // 公众号类型
-  const checkAccountType = (params = {}) => $axios.get('/wx/sdk/account/type', { params })
+  const checkAccountType = (params = {}) =>
+    $axios.get('/wx/sdk/account/type', { params })
+  // 判断用户手机号是否绑定了机构信息
+  const fetchClubAuthInfo = (params = {}) =>
+    $axios.get('/wx/user/info', { params })
+  // 获取品牌列表
+  const fetchBrandList = (params = {}) =>
+    $axios.get('/wx/auth/brand/list', { params })
+  // 获取设备分类列表
+  const fetchProductSelectList = (params = {}) =>
+    $axios.get('/wx/auth/product/type/select', { params })
+  // 机构注册(全部信息注册)
+  const clubUserRegisterAll = (data = {}) =>
+    $axios.post('/wx/user/register/all', data)
+  // 机构账号注册
+  const clubUserRegister = (data = {}) =>
+    $axios.post('/wx/user/register/simple', data)
+  // 机构账号密码找回
+  const clubUserReset = (data = {}) =>
+    $axios.post('/wx/user/password/update', data)
+  // 机构账号登录
+  const clubUserLogin = (data = {}) =>
+    $axios.post('/wx/user/login/password', data)
+  // 机构账号验证码
+  const clubUserCodeSend = (data = {}) =>
+    $axios.post('/wx/user/login/code/send', data)
+  // 查询机构认证信息
+  const fetchClubAuthInfoData = (params = {}) =>
+    $axios.get('/wx/auth/form/data', { params })
+  // 查询机构认证信息
+  const authClubSave = (data = {}) => $axios.post('/wx/auth/save', data)
+  // 查询授权商品列表
+  const getClubAuthProductList = (params = {}) =>
+    $axios.get('/wx/auth/product/list', { params })
+  // 查询授权商品列表
+  const getProductDetails = (params = {}) =>
+    $axios.get('/wx/auth/product/form/data', { params })
+  // 编辑保存授权商品
+  const authProducSave = (data = {}) =>
+    $axios.post('/wx/auth/product/save', data)
   // 高德地图api : 将坐标转化为高德地图坐标
   const assistant = (params = {}) =>
-    fetch('https://restapi.amap.com/v3/assistant/coordinate/convert' + queryStringify(params))
+    fetch(
+      'https://restapi.amap.com/v3/assistant/coordinate/convert' +
+        queryStringify(params)
+    )
 
   return {
     customLogin,
@@ -86,6 +151,19 @@ export default ($axios) => {
     fetchDoctorList,
     fetchDoctorDetail,
     checkAccountType,
-    assistant
+    assistant,
+    fetchClubAuthInfo,
+    fetchBrandList,
+    fetchProductSelectList,
+    clubUserRegisterAll,
+    clubUserRegister,
+    clubUserReset,
+    clubUserLogin,
+    clubUserCodeSend,
+    fetchClubAuthInfoData,
+    authClubSave,
+    getClubAuthProductList,
+    getProductDetails,
+    authProducSave,
   }
-}
+}

+ 6 - 2
components/LdmLogin/index.vue

@@ -193,7 +193,9 @@ export default {
           top: 50%;
           transform: translateY(-50%);
           font-size: 16px;
-          color: #a62645;
+          @include themify($themes) {
+            color: themed('color');
+          }
           cursor: pointer;
         }
       }
@@ -265,7 +267,9 @@ export default {
           top: 50%;
           transform: translateY(-50%);
           font-size: 3.2vw;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           cursor: pointer;
         }
       }

+ 44 - 0
components/SimpleAMap/common/utils.js

@@ -0,0 +1,44 @@
+export function initGeocoder() {
+  return new Promise((resolve, reject) => {
+    window.AMap.plugin('AMap.Geocoder', () => {
+      try {
+        const geocoder = new window.AMap.Geocoder({
+          // city 指定进行编码查询的城市,支持传入城市名、adcode 和 citycode
+          city: '全国',
+        })
+        resolve(geocoder)
+      } catch (error) {
+        reject(error)
+      }
+    })
+  })
+}
+
+export function geolocation() {
+  return new Promise((resolve, reject) => {
+    window.AMap.plugin('AMap.Geolocation', () => {
+      const geolocation = new window.AMap.Geolocation({
+        // 是否使用高精度定位,默认:true
+        enableHighAccuracy: true,
+        // 设置定位超时时间,默认:无穷大
+        timeout: 10000,
+        // 定位按钮的停靠位置的偏移量
+        offset: [10, 20],
+        //  定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
+        zoomToAccuracy: true,
+        //  定位按钮的排放位置,  RB表示右下
+        position: 'RB',
+      })
+
+      geolocation.getCurrentPosition((status, result) => {
+        if (status === 'complete') {
+          resolve(result)
+        } else {
+          reject(result)
+        }
+      })
+    })
+  })
+}
+
+export default {}

+ 160 - 0
components/SimpleAMap/index.vue

@@ -0,0 +1,160 @@
+<template>
+  <div class="a-map" id="aMap"></div>
+</template>
+
+<script>
+import { geolocation, initGeocoder } from './common/utils'
+export default {
+  name: 'AMap',
+  props: {
+    zoom: {
+      type: Number,
+      default: 17
+    },
+    markerIcon: {
+      type: String,
+      default: '/location.png'
+    },
+    center: {
+      type: Boolean,
+      default: false
+    },
+    address: {
+      type: String,
+      default: ''
+    },
+    lnglat: {
+      type: Array,
+      default: null
+    }
+  },
+  data () {
+    return {
+      marker: null,
+      map: null,
+      timer: null,
+      geocoder: null
+    }
+  },
+  methods: {
+    async init () {
+      if (this.lnglat) {
+        console.log('经纬度定位')
+        this.initMap(this.lnglat)
+      } else if (this.address) {
+        console.log('地址定位')
+        this.initGeocoder()
+      } else {
+        console.log('高精度自动定位')
+        this.geolocation()
+      }
+    },
+    // 高精度定位
+    async geolocation () {
+      try {
+        const result = await geolocation()
+        this.$emit('position', {
+          lng: result.position.lng,
+          lat: result.position.lat
+        })
+        this.initMap(result.position)
+      } catch (error) {
+        this.initMap()
+        alert('获取当前位置信息失败,已为您定位到当前城市!')
+        console.log('获取当前位置信息失败,已为您定位到当前城市!')
+      }
+    },
+    // 根据地址信息定位
+    async initGeocoder () {
+      try {
+        this.geocoder = await initGeocoder()
+        console.log(this.geocoder)
+
+        this.geocoder.getLocation(this.address, (status, result) => {
+          if (status === 'complete' && result.info === 'OK') {
+            const position = result.geocodes[0].location
+            this.initMap(position)
+            this.$emit('position', {
+              lng: position.lng,
+              lat: position.lat
+            })
+          }
+        })
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 初始化地图
+    initMap (position) {
+      this.map = new window.AMap.Map('aMap', {
+        viewMode: '2D', // 默认使用 2D 模式,如果希望使用带有俯仰角的 3D 模式,请设置 viewMode: '3D',
+        zoom: this.zoom, // 初始化地图层级
+        center: position // 初始化地图中心点
+      })
+      this.map.on('click', this.onMapClick)
+      if (position) {
+        this.map.add(this.initMarker(position))
+      }
+      this.initMapControl()
+    },
+    // 初始化标记点
+    initMarker (position) {
+      this.marker = new window.AMap.Marker({
+        icon: this.markerIcon,
+        position,
+        anchor: 'bottom-center',
+        draggable: true
+      })
+      this.marker.on('dragging', this.moveMarker)
+      return this.marker
+    },
+    // 修改标记点
+    onMapClick (map) {
+      if (this.marker) this.map.remove(this.marker)
+      if (this.center) this.map.setCenter(map.lnglat)
+      this.map.add(this.initMarker(map.lnglat))
+      this.$emit('position', {
+        lng: map.lnglat.lng,
+        lat: map.lnglat.lat
+      })
+    },
+    // 标记点移动
+    moveMarker (marker) {
+      if (this.timer) clearTimeout(this.timer)
+      this.timer = setTimeout(() => {
+        if (this.center) this.map.setCenter(marker.lnglat)
+        this.$emit('position', {
+          lng: marker.lnglat.lng,
+          lat: marker.lnglat.lat
+        })
+      }, 200)
+    },
+    // 地图控件
+    initMapControl () {
+      window.AMap.plugin(
+        ['AMap.ToolBar', 'AMap.Scale', 'AMap.MapType', 'AMap.Geolocation'],
+        () => {
+          // 在图面添加工具条控件,工具条控件集成了缩放、平移、定位等功能按钮在内的组合控件
+          this.map.addControl(new window.AMap.ToolBar())
+
+          // 在图面添加比例尺控件,展示地图在当前层级和纬度下的比例尺
+          this.map.addControl(new window.AMap.Scale())
+
+          // 在图面添加类别切换控件,实现默认图层与卫星图、实施交通图层之间切换的控制
+          this.map.addControl(new window.AMap.MapType())
+
+          // 在图面添加定位控件,用来获取和展示用户主机所在的经纬度位置
+          // this.map.addControl(new window.AMap.Geolocation())
+        }
+      )
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+#aMap {
+  height: 100%;
+  min-height: 400px;
+}
+</style>

+ 15 - 0
components/SimpleDeviceParams/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="simple-device-params">
+    
+  </div>
+</template>
+
+<script>
+export default {
+
+}
+</script>
+
+<style>
+
+</style>

+ 245 - 0
components/SimpleDialog/index.vue

@@ -0,0 +1,245 @@
+<template>
+  <div class="simple-dialog" v-if="value">
+    <div class="simple-dialog__container">
+      <span
+        class="el-icon-close simple-dialog__colse"
+        @click="onCloseClick"
+      ></span>
+      <div class="simple-dialog__title">
+        <span class="simple-dialog__title__text">提示</span>
+        <slot name="title"></slot>
+      </div>
+      <div
+        class="simple-dialog__content"
+        :style="{ textAlign: center && 'center' }"
+      >
+        <slot></slot>
+        <span class="simple-dialog__content__text" v-text="description"></span>
+      </div>
+      <div class="simple-dialog__footer">
+        <slot name="footer" v-if="!confirm || !cancel"></slot>
+        <div
+          class="simple-dialog__confirm simple-dialog__btn"
+          v-if="confirm"
+          @click="onConfirm"
+        >
+          {{ confirmText }}
+        </div>
+        <div
+          class="simple-dialog__cancel simple-dialog__btn"
+          v-if="cancel"
+          @click="onCancel"
+        >
+          {{ cancelText }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  model: {
+    prop: 'value',
+    event: 'change',
+  },
+  props: {
+    confirmText: {
+      type: String,
+      default: '确定',
+    },
+    cancelText: {
+      type: String,
+      default: '取消',
+    },
+    value: {
+      type: Boolean,
+      default: false,
+    },
+    confirm: {
+      type: Boolean,
+      default: true,
+    },
+    cancel: {
+      type: Boolean,
+      default: true,
+    },
+    description: {
+      type: String,
+      default: '',
+    },
+    center: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  methods: {
+    onCloseClick() {
+      this.$emit('change', false)
+    },
+    onConfirm() {
+      this.$emit('confirm')
+    },
+    onCancel() {
+      this.$emit('cancel')
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .simple-dialog {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100vh;
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 10001;
+    background: rgba(0, 0, 0, 0.39);
+
+    .simple-dialog__container {
+      position: relative;
+      width: 400px;
+      background: #fff;
+
+      .simple-dialog__colse {
+        width: 36px;
+        height: 36px;
+        font-size: 26px;
+        position: absolute;
+        top: 8px;
+        right: 12px;
+        text-align: center;
+        line-height: 36px;
+        color: #c2c2c2;
+        cursor: pointer;
+      }
+
+      .simple-dialog__title {
+        padding: 12px 24px;
+        border-bottom: 1px solid #c2c2c2;
+        .simple-dialog__title__text {
+          font-size: 18px;
+          color: #282828;
+        }
+      }
+
+      .simple-dialog__content {
+        padding: 24px 24px;
+
+        .simple-dialog__content__text {
+          font-size: 16px;
+          color: #282828;
+        }
+      }
+
+      .simple-dialog__footer {
+        padding: 0 24px 24px;
+
+        .simple-dialog__btn {
+          width: 100%;
+          height: 40px;
+          text-align: center;
+          line-height: 40px;
+          font-size: 16px;
+          border-radius: 4px;
+          cursor: pointer;
+        }
+
+        .simple-dialog__confirm {
+          background: #f3920d;
+          color: #fff;
+        }
+
+        .simple-dialog__cancel {
+          color: #666666;
+          box-sizing: border-box;
+          border: 1px solid #c2c2c2;
+          margin-top: 16px;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .simple-dialog {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100vh;
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 10001;
+    background: rgba(0, 0, 0, 0.39);
+
+    .simple-dialog__container {
+      position: relative;
+      width: 76vw;
+      min-height: 20vw;
+      background: #fff;
+
+      .simple-dialog__colse {
+        width: 6.4vw;
+        height: 6.4vw;
+        font-size: 5vw;
+        position: absolute;
+        top: 3vw;
+        right: 3vw;
+        text-align: center;
+        line-height: 6.4vw;
+        color: #c2c2c2;
+      }
+
+      .simple-dialog__title {
+        padding: 3vw 4.8vw;
+        border-bottom: 1px solid #c2c2c2;
+        .simple-dialog__title__text {
+          font-size: 4.2vw;
+          color: #282828;
+        }
+      }
+
+      .simple-dialog__content {
+        padding: 3vw 4.8vw;
+
+        .simple-dialog__content__text {
+          font-size: 3.6vw;
+          color: #282828;
+        }
+      }
+
+      .simple-dialog__footer {
+        padding: 0 24px 24px;
+
+        .simple-dialog__btn {
+          width: 100%;
+          height: 8.8vw;
+          text-align: center;
+          line-height: 8.8vw;
+          font-size: 3.6vw;
+          border-radius: 0.4vw;
+        }
+
+        .simple-dialog__confirm {
+          background: #f3920d;
+          color: #fff;
+        }
+
+        .simple-dialog__cancel {
+          color: #666666;
+          box-sizing: border-box;
+          border: 1px solid #c2c2c2;
+          margin-top: 2.8vw;
+        }
+      }
+    }
+  }
+}
+</style>

+ 245 - 27
components/SimpleLogin/index.vue

@@ -4,7 +4,15 @@
       <div class="wrapper flex justify-center items-center" @click.stop>
         <div class="block flex items-center flex-col py-6">
           <div class="close" @click="onClose"></div>
-          <div class="title pb-6">登录</div>
+          <div class="title pb-6">
+            {{
+              formType === 'login'
+                ? '登录'
+                : formType === 'register'
+                ? '注册'
+                : '忘记密码'
+            }}
+          </div>
           <div class="form">
             <div class="form-item mb-4">
               <input
@@ -14,7 +22,7 @@
                 maxlength="11"
               />
             </div>
-            <div class="form-item mb-4 code">
+            <div class="form-item mb-4 code" v-if="formType !== 'login'">
               <input
                 type="text"
                 placeholder="验证码"
@@ -24,7 +32,29 @@
               />
               <span class="send" @click="onSend">{{ sendCodeBtnText }}</span>
             </div>
-            <div class="submit" @click="onSubmit">登录</div>
+            <div class="form-item mb-4">
+              <input
+                type="password"
+                placeholder="密码"
+                v-model="formData.password"
+                maxlength="11"
+              />
+            </div>
+            <div class="form-item mb-4" v-if="formType !== 'login'">
+              <input
+                type="password"
+                placeholder="确认密码"
+                v-model="formData.confirmPwd"
+              />
+            </div>
+            <div class="submit" @click="onSubmit">{{ submitText }}</div>
+            <div
+              class="flex justify-between control mt-2"
+              v-if="formType === 'login'"
+            >
+              <span class="forget" @click="onForgetPwd">忘记密码</span>
+              <span class="regist" @click="onRegister">立即注册</span>
+            </div>
           </div>
         </div>
       </div>
@@ -37,14 +67,23 @@ import { mapGetters } from 'vuex'
 import { isMobile } from '@/utils/validator'
 export default {
   name: 'simple-login',
+  props: {
+    type: {
+      type: String,
+      default: 'login',
+    },
+  },
   data() {
     return {
+      formType: 'login',
       show: false,
       sendStatus: 0,
       formData: {
         authUserId: '',
         mobile: '',
         verifyCode: '',
+        password: '',
+        confirmPwd: '',
       },
       timer: null,
     }
@@ -56,48 +95,172 @@ export default {
         ? '发送验证码'
         : `再次发送${this.sendStatus}s`
     },
+    submitText() {
+      return this.formType === 'login'
+        ? '登录'
+        : this.formType === 'forget'
+        ? '确定'
+        : '提交'
+    },
+  },
+  created() {
+    this.formType = this.type
   },
   watch: {
     loginVisiable() {
       this.show = this.loginVisiable
     },
+    type: {
+      handler: function (nval) {
+        this.formType = nval
+        console.log(nval)
+      },
+      immediate: true,
+    },
   },
   methods: {
+    // 忘记密码
+    onForgetPwd() {
+      this.$emit('click', 'forget')
+    },
+    // 立即注册
+    onRegister() {
+      this.$emit('click', 'register')
+    },
+
     async onSubmit() {
       // 验证手机号是否合法
       if (!isMobile(this.formData.mobile)) {
         this.$toast('请输入正确的手机号')
         return
       }
+
+      if (this.formType === 'register') {
+        this.onRegisterSubmit()
+      } else if (this.formType === 'forget') {
+        this.onForgetSubmit()
+      } else {
+        this.onLoginSubmit()
+      }
+    },
+
+    // 忘记密码
+    async onForgetSubmit() {
       if (!this.formData.verifyCode) {
         this.$toast('请输入验证码')
         return
       }
+
+      if (this.formData.password.length < 6) {
+        this.$toast('密码长度需大于6位')
+        return
+      }
+
+      if (this.formData.password !== this.formData.confirmPwd) {
+        this.$toast('两次输入的密码不一致')
+        return
+      }
+
       try {
-        this.formData.authUserId = this.authUserId
-        const res = await this.$http.api.customLogin(this.formData)
-        this.$store.dispatch('user/login', res.data)
-        this.$setStorage(this.routePrefix, 'userInfo', res.data)
-        // 关闭登录窗口
-        this.onClose()
-        // 重定向
-        const login_redicret = this.$getStorage(
-          this.routePrefix,
-          'login_redicret'
-        )
-        if (login_redicret && login_redicret !== this.$route.path) {
-          this.$router.push(login_redicret)
-        }
+        await this.$http.api.clubUserReset({
+          mobile: this.formData.mobile,
+          verifyCode: this.formData.verifyCode,
+          password: this.formData.password,
+          authUserId: this.authUserId,
+        })
+        this.$toast('密码修改成功')
+        this.$emit('click', 'login')
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 用户注册
+    async onRegisterSubmit() {
+      if (!this.formData.verifyCode) {
+        this.$toast('请输入验证码')
+        return
+      }
+      if (this.formData.password.length < 6) {
+        this.$toast('密码长度需大于6位')
+        return
+      }
+      if (this.formData.password !== this.formData.confirmPwd) {
+        this.$toast('两次输入的密码不一致')
+        return
+      }
+
+      try {
+        await this.$http.api.clubUserRegister({
+          mobile: this.formData.mobile,
+          verifyCode: this.formData.verifyCode,
+          password: this.formData.password,
+          authUserId: this.authUserId,
+        })
+        this.$toast('注册成功,请登录')
+        this.$emit('click', 'login')
       } catch (error) {
         console.log(error)
+        this.$toast(error.msg)
       }
     },
+
+    // 用户登录
+    async onLoginSubmit() {
+      if (!this.formData.password) {
+        this.$toast('密码不能为空')
+        return
+      }
+
+      try {
+        const res = await this.$http.api.clubUserLogin({
+          mobile: this.formData.mobile,
+          password: this.formData.password,
+          authUserId: this.authUserId,
+        })
+        this.login(res)
+      } catch (error) {
+        console.log(error)
+        this.$toast(error.msg)
+      }
+    },
+
+    login(res) {
+      this.$store.dispatch('user/login', res.data)
+      this.$setStorage(this.routePrefix, 'userInfo', res.data)
+      // 关闭登录窗口
+      this.onClose()
+      // this.$router.push(this.routePrefix)
+
+      const clubRegisterLink = this.$getStorage(
+        this.routePrefix,
+        'club-register-link'
+      )
+      if (clubRegisterLink) {
+        this.$removeStorage(this.routePrefix, 'club-register-link')
+        this.$setStorage(this.routePrefix, 'bind-flag', true)
+        this.$router.push(clubRegisterLink)
+      } else {
+        this.$router.push(this.routePrefix)
+      }
+
+      // 重定向
+      // const login_redicret = this.$getStorage(
+      //   this.routePrefix,
+      //   'login_redicret'
+      // )
+      // if (login_redicret && login_redicret !== this.$route.path) {
+      //   this.$router.push(login_redicret)
+      // }
+    },
+
     // 关闭登录窗口
     onClose() {
       this.$store.commit('app/HIDE_LOGIN')
       this.formData.mobile = ''
       this.formData.verifyCode = ''
-      this.formData.authUserId = ''
+      this.formData.password = ''
+      this.formData.confirmPwd = ''
     },
     async onSend() {
       if (this.sendStatus > 0) return
@@ -108,10 +271,10 @@ export default {
       }
       try {
         // 发送验证码
-        const res = await this.$http.api.sendVerifyCode({
+        await this.$http.api.clubUserCodeSend({
           mobile: this.formData.mobile,
           authUserId: this.authUserId,
-          type: 1,
+          type: this.formType === 'register' ? 1 : 2,
         })
         this.$toast('验证码已发送')
         // 开启倒计时
@@ -183,14 +346,39 @@ export default {
           top: 50%;
           transform: translateY(-50%);
           font-size: 16px;
-          color: #bc1724;
           cursor: pointer;
+          @include themify($themes) {
+            color: themed('color');
+          }
         }
       }
+
+      .control {
+        font-size: 14px;
+
+        .forget,
+        .regist {
+          cursor: pointer;
+        }
+        .forget {
+          color: #666;
+
+          &:hover {
+            @include themify($themes) {
+              color: themed('color');
+            }
+          }
+        }
+        .regist {
+          @include themify($themes) {
+            color: themed('color');
+          }
+        }
+      }
+
       .submit {
         width: 326px;
         height: 46px;
-        background: #bc1724;
         font-size: 16px;
         color: #fff;
         text-align: center;
@@ -198,8 +386,14 @@ export default {
         transition: all 0.4s;
         cursor: pointer;
 
+        @include themify($themes) {
+          background: themed('color');
+        }
+
         &:hover {
-          background-color: #960915;
+          @include themify($themes) {
+            background: themed('hover-color');
+          }
         }
       }
     }
@@ -253,22 +447,46 @@ export default {
           top: 50%;
           transform: translateY(-50%);
           font-size: 3.2vw;
-          color: #bc1724;
+          cursor: pointer;
+          @include themify($themes) {
+            color: themed('color');
+          }
+        }
+      }
+
+      .control {
+        font-size: 3.2vw;
+
+        .forget,
+        .regist {
           cursor: pointer;
         }
+        .forget {
+          color: #666;
+
+          &:hover {
+            @include themify($themes) {
+              color: themed('color');
+            }
+          }
+        }
+        .regist {
+          @include themify($themes) {
+            color: themed('color');
+          }
+        }
       }
       .submit {
         width: 62vw;
         height: 8.8vw;
-        background: #bc1724;
         font-size: 3.2vw;
         color: #fff;
         text-align: center;
         line-height: 8.8vw;
         transition: all 0.4s;
 
-        &:hover {
-          background-color: #b60c1a;
+        @include themify($themes) {
+          background: themed('color');
         }
       }
     }

+ 16 - 5
components/SimplePagination/index.vue

@@ -90,11 +90,16 @@ export default {
           .van-pagination__item {
             margin-left: 4px;
             margin-right: 4px;
-            color: #bc1724;
+
+            @include themify($themes) {
+              color: themed('color');
+            }
 
             &:active {
               color: #fff;
-              background-color: #bc1724;
+              @include themify($themes) {
+                background-color: themed('color');
+              }
             }
           }
 
@@ -107,12 +112,16 @@ export default {
             }
 
             &:hover {
-              color: #bc1724;
+              @include themify($themes) {
+                color: themed('color');
+              }
             }
           }
 
           .van-pagination__item--active {
-            background-color: #bc1724 !important;
+            @include themify($themes) {
+              background-color: themed('color') !important;
+            }
             color: #fff;
           }
         }
@@ -134,7 +143,9 @@ export default {
 
           &:active {
             color: #fff;
-            background-color: #bc1724;
+            @include themify($themes) {
+              background-color: themed('color');
+            }
           }
         }
       }

+ 272 - 0
components/SimpleRadio/index.vue

@@ -0,0 +1,272 @@
+<template>
+  <div class="simple-radio">
+    <template v-for="(item, index) in list">
+      <div
+        class="simple-radio__item"
+        :class="[
+          'simple-radio-theme-' + type,
+          { active: item.value === value },
+        ]"
+        :key="index"
+        @click="onClick(item)"
+      >
+        <span class="simple-radio__con"></span>
+        <span class="simple-radio__label" v-text="item.name"></span>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+export default {
+  model: {
+    prop: 'value',
+    event: 'change',
+  },
+  props: {
+    value: {
+      type: Number,
+      default: 0,
+    },
+    list: {
+      type: Array,
+      default: [],
+    },
+    type: {
+      type: String,
+      default: 'defalut',
+    },
+  },
+  methods: {
+    onClick(item) {
+      this.$emit('change', item.value)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+// pc端
+@media screen and (min-width: 768px) {
+  .simple-radio {
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+    flex-wrap: wrap;
+
+    .simple-radio__item {
+      cursor: pointer;
+      margin-right: 40px;
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+      .simple-radio__label {
+        font-size: 14px;
+        color: #666;
+      }
+
+      &.simple-radio-theme-rect {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 117px;
+        height: 46px;
+        box-sizing: border-box;
+        border: 1px solid #c2c2c2;
+        border-radius: 2px;
+        position: relative;
+        overflow: hidden;
+
+        &.active {
+          @include themify($themes) {
+            border-color: themed('color');
+          }
+
+          .simple-radio__con {
+            position: absolute;
+            right: -20px;
+            bottom: -20px;
+            width: 40px;
+            height: 40px;
+
+            color: #fff;
+            transform: rotateZ(45deg);
+            @include themify($themes) {
+              background: themed('color');
+            }
+
+            &::after {
+              content: '选中';
+              position: absolute;
+              left: -5px;
+              top: 0;
+              display: block;
+              font-size: 10px;
+              transform: rotateZ(-90deg) scale(0.8);
+            }
+          }
+        }
+      }
+
+      &.simple-radio-theme-defalut {
+        position: relative;
+        padding-left: 24px;
+
+        &.active {
+          .simple-radio__con {
+            &::before {
+              @include themify($themes) {
+                background: themed('color');
+              }
+            }
+          }
+        }
+
+        .simple-radio__con {
+          position: absolute;
+          left: 0;
+          top: 50%;
+          width: 18px;
+          height: 18px;
+          box-sizing: border-box;
+          border-radius: 50%;
+          transform: translateY(-50%);
+
+          @include themify($themes) {
+            border: 1px solid themed('color');
+          }
+
+          &::before {
+            display: block;
+            content: '';
+            width: 8px;
+            height: 8px;
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            border-radius: 50%;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动端
+@media screen and (max-width: 768px) {
+  .simple-radio {
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+    flex-wrap: wrap;
+
+    .simple-radio__item {
+      cursor: pointer;
+
+      .simple-radio__label {
+        font-size: 3.4vw;
+        color: #666;
+      }
+
+      &.simple-radio-theme-rect {
+        margin-right: 2.4vw;
+        margin-bottom: 2.4vw;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 27vw;
+        height: 11.8vw;
+        box-sizing: border-box;
+        border: 1px solid #c2c2c2;
+        border-radius: 0.4vw;
+        position: relative;
+        overflow: hidden;
+
+        &:nth-child(3n) {
+          margin-right: 0;
+        }
+
+        &.active {
+          @include themify($themes) {
+            border-color: themed('color');
+          }
+
+          .simple-radio__con {
+            position: absolute;
+            right: -20px;
+            bottom: -20px;
+            width: 40px;
+            height: 40px;
+
+            color: #fff;
+            transform: rotateZ(45deg);
+            @include themify($themes) {
+              background: themed('color');
+            }
+
+            &::after {
+              content: '选中';
+              position: absolute;
+              left: -5px;
+              top: 0;
+              display: block;
+              font-size: 10px;
+              transform: rotateZ(-90deg) scale(0.8);
+            }
+          }
+        }
+      }
+
+      &.simple-radio-theme-defalut {
+        position: relative;
+        padding-left: 6vw;
+        margin-right: 7.2vw;
+
+        &:nth-child(4n) {
+          margin-right: 0;
+        }
+
+        &.active {
+          .simple-radio__con {
+            &::before {
+              @include themify($themes) {
+                background: themed('color');
+              }
+            }
+          }
+        }
+
+        .simple-radio__con {
+          position: absolute;
+          left: 0;
+          top: 50%;
+          width: 3.6vw;
+          height: 3.6vw;
+          box-sizing: border-box;
+          border-radius: 50%;
+          transform: translateY(-50%);
+
+          @include themify($themes) {
+            border: 1px solid themed('color');
+          }
+
+          &::before {
+            display: block;
+            content: '';
+            width: 1.8vw;
+            height: 1.8vw;
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            border-radius: 50%;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 98 - 0
components/SimpleStep/index.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="simple-step">
+    <div
+      class="simple-step__item"
+      v-for="(item, index) in list"
+      :key="index"
+      :class="{ active: item.id === active }"
+    >
+      <span v-text="item[label]"></span>
+      <span class="simple-step__line"></span>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'simple-step',
+  props: {
+    list: {
+      type: Array,
+      default: [],
+    },
+    label: {
+      type: String,
+      default: 'label',
+    },
+    active: {
+      type: Number,
+      default: 0,
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+// pc端
+@media screen and (min-width: 768px) {
+  .simple-step {
+    display: flex;
+    justify-content: space-evenly;
+    align-items: center;
+    padding: 24px 0;
+
+    .simple-step__item {
+      font-size: 24px;
+      color: #282828;
+
+      position: relative;
+      padding-bottom: 4px;
+      &.active {
+        .simple-step__line {
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          width: 100%;
+          height: 3px;
+          @include themify($themes) {
+            background-color: themed('color');
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动端
+@media screen and (max-width: 768px) {
+  .simple-step {
+    display: flex;
+    justify-content: space-evenly;
+    align-items: center;
+    padding: 8vw 0 6vw;
+
+    .simple-step__item {
+      font-size: 4.2vw;
+      color: #999;
+
+      position: relative;
+      padding-bottom: 1.2vw;
+
+      &.active {
+        color: #282828;
+
+        .simple-step__line {
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          width: 100%;
+          height: 0.5vw;
+          @include themify($themes) {
+            background-color: themed('color');
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 12 - 4
components/SimpleTabs/index.vue

@@ -100,9 +100,13 @@ export default {
         }
 
         &.active {
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           &::after {
-            background-color: #bc1724;
+            @include themify($themes) {
+              background-color: themed('color');
+            }
           }
         }
       }
@@ -143,9 +147,13 @@ export default {
 
         &.active {
           font-size: 4.6vw;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           &::after {
-            background-color: #bc1724;
+            @include themify($themes) {
+              background-color: themed('color');
+            }
           }
         }
       }

+ 119 - 0
components/SimpleUploadImage/index.vue

@@ -0,0 +1,119 @@
+<template>
+  <div>
+    <el-upload
+      :class="{ 'el-upload-hidden': !chooseState }"
+      :list-type="listType"
+      :action="action"
+      :headers="headers"
+      :on-success="uploadImageSuccess"
+      :on-remove="handleImageRemove"
+      :before-upload="beforeUpload"
+      :on-error="uploadError"
+      :on-preview="handlePictureCardPreview"
+      :limit="limit"
+      :multiple="multiple"
+      :accept="accept"
+      :file-list="imageList"
+      :disabled="disabled"
+    >
+      <div v-if="tip" slot="tip" class="el-upload__tip">{{ tip }}</div>
+      <i slot="default" class="el-icon-plus" />
+    </el-upload>
+    <el-dialog :visible.sync="dialogVisible">
+      <img width="100%" :src="dialogImageUrl" />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+// import { mapGetters } from 'vuex'
+export default {
+  name: 'SimpleUploadImage',
+  props: {
+    tip: {
+      type: String,
+      default: '',
+    },
+    multiple: {
+      type: Boolean,
+      default: false,
+    },
+    limit: {
+      type: Number,
+      default: 1,
+    },
+    accept: {
+      type: String,
+      default: '.jpg,.png,.gif',
+    },
+    listType: {
+      type: String,
+      default: 'picture-card',
+    },
+    imageList: {
+      type: Array,
+      default: () => [],
+    },
+    uuid: {
+      type: Number,
+      default: 0,
+    },
+    beforeUpload: {
+      type: Function,
+      default: () => true,
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      dialogImageUrl: '',
+    }
+  },
+  computed: {
+    // ...mapGetters(['token']),
+    chooseState() {
+      return this.imageList.length < this.limit
+    },
+    action() {
+      return process.env.BASE_URL + '/wx/upload/image'
+    },
+    headers() {
+      return {
+        // 'X-Token': this.token,
+      }
+    },
+  },
+  methods: {
+    // 上传成功
+    uploadImageSuccess(response, file, fileList) {
+      this.$emit('success', { response, file, fileList })
+    },
+    // 删除
+    handleImageRemove(file, fileList) {
+      this.$emit('remove', { file, fileList })
+    },
+    // 上传失败
+    uploadError(err, file, fileList) {
+      this.$emit('error', { err, file, fileList })
+    },
+    handlePictureCardPreview(file) {
+      this.dialogImageUrl = file.url
+      this.dialogVisible = true
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep {
+  .el-upload-hidden {
+    .el-upload {
+      display: none;
+    }
+  }
+}
+</style>

+ 69 - 29
layouts/app.vue

@@ -1,36 +1,47 @@
 <template>
-  <div class="layout" v-if="isMounted">
-    <div class="header">
-      <div class="navbar flex justify-between items-center">
-        <div class="logo flex items-center" @click="backHome">
-          <img
-            src="https://static.caimei365.com/www/authentic/h5/icon-logo.png"
-          />
-          <span>认证通</span>
-        </div>
-        <div class="user-info">
-          <template v-if="accessToken">
-            <span v-text="userInfo.mobile"></span>
-            <span class="underline logout" @click="logout">退出登录</span>
-          </template>
-          <template v-else>
-            <div
-              class="login pr-3 pl-3 border rounded-sm border-white leading-6"
-              @click="onLogin"
-            >
-              登录
-            </div>
-          </template>
+  <div :class="themeClass">
+    <div class="layout" v-if="isMounted">
+      <div class="header">
+        <div class="navbar flex justify-between items-center">
+          <div class="logo flex items-center" @click="backHome">
+            <img
+              src="https://static.caimei365.com/www/authentic/h5/icon-logo.png"
+            />
+            <span>认证通</span>
+          </div>
+          <div class="user-info">
+            <template v-if="accessToken">
+              <span v-text="userInfo.mobile"></span>
+              <span class="underline logout" @click="logout">退出登录</span>
+            </template>
+            <template v-else>
+              <div class="flex justify-center">
+                <div
+                  class="login pr-3 pl-3 rounded-sm border-white leading-6"
+                  @click="onLogin"
+                >
+                  登录
+                </div>
+                |
+                <div
+                  class="register pr-3 pl-3 rounded-sm border-white leading-6"
+                  @click="onRegister"
+                >
+                  注册
+                </div>
+              </div>
+            </template>
+          </div>
         </div>
       </div>
+      <div class="content">
+        <nuxt />
+      </div>
+      <div class="footer flex justify-center items-center">
+        - 由采美网提供技术支持 -
+      </div>
+      <SimpleLogin :type="formType" @click="onLoginClick"></SimpleLogin>
     </div>
-    <div class="content">
-      <nuxt />
-    </div>
-    <div class="footer flex justify-center items-center">
-      - 由采美网提供技术支持 -
-    </div>
-    <SimpleLogin></SimpleLogin>
   </div>
 </template>
 
@@ -47,7 +58,11 @@ export default {
       'appId',
       'accountType',
       'routePrefix',
+      'themeName',
     ]),
+    themeClass() {
+      return `theme-${this.themeName}`
+    },
   },
   head() {
     return {
@@ -57,6 +72,7 @@ export default {
   data() {
     return {
       isMounted: false,
+      formType: 'login',
     }
   },
   mounted() {
@@ -67,6 +83,10 @@ export default {
     window.removeEventListener('resize', () => {})
   },
   methods: {
+    onLoginClick(type) {
+      this.formType = type
+    },
+
     init() {
       this.responseWidth()
       this.initPageData()
@@ -81,6 +101,11 @@ export default {
       // 保存用户AppId
       this.$store.commit('user/SET_AUTH_USER_ID', authUserId)
 
+      // 设置页面主题
+      if (authUserId === parseInt(12)) {
+        this.$store.commit('app/SET_PAGE_THEME', 'ross')
+      }
+
       // 获取用户信息
       let userInfo = this.$getStorage(routePrefix, 'userInfo')
       if (userInfo && userInfo.authUserId === authUserId) {
@@ -131,6 +156,13 @@ export default {
         }
         return toAuthorization(this.appId, payload)
       }
+      this.formType = 'login'
+      this.$store.commit('app/SHOW_LOGIN')
+    },
+
+    onRegister() {
+      this.formType = 'register'
+      console.log(this.formType)
       this.$store.commit('app/SHOW_LOGIN')
     },
 
@@ -170,6 +202,7 @@ export default {
   .layout {
     padding-top: 80px;
     user-select: none;
+
     .header {
       position: fixed;
       top: 0;
@@ -204,8 +237,15 @@ export default {
         font-size: 16px;
 
         .login,
+        .register,
         .logout {
           cursor: pointer;
+
+          &:hover {
+            @include themify($themes) {
+              color: themed('color');
+            }
+          }
         }
       }
     }

+ 19 - 1
nuxt.config.js

@@ -21,14 +21,28 @@ export default {
       { name: 'format-detection', content: 'telephone=no' },
     ],
     // link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
+    script: [
+      {
+        src: '/map.config.js',
+      },
+      {
+        src: 'https://webapi.amap.com/maps?v=2.0&key=eae3be059db26dc1f9cae1d1bee9d4cb',
+      },
+    ],
   },
 
   // Global CSS: https://go.nuxtjs.dev/config-css
-  css: ['vant/lib/index.css', 'swiper/css/swiper.css', '@/styles/global.css'],
+  css: [
+    'vant/lib/index.css',
+    'element-ui/lib/theme-chalk/index.css',
+    'swiper/css/swiper.css',
+    '@/styles/global.css',
+  ],
 
   // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
   plugins: [
     '@/plugins/vant',
+    '@/plugins/element-ui',
     '@/plugins/axios',
     '@/plugins/vue-filters',
     '@/plugins/storage',
@@ -39,6 +53,7 @@ export default {
 
   // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
   buildModules: [
+    '@nuxtjs/style-resources',
     // https://go.nuxtjs.dev/tailwindcss
     '@nuxtjs/tailwindcss',
   ],
@@ -61,6 +76,9 @@ export default {
     filenames: {
       chunk: ({ isDev }) => (isDev ? '[name].js' : '[id].[contenthash].js'),
     },
+    styleResources: {
+      scss: './themes/themeMixin.scss',
+    },
   },
 
   // 配置 Nuxt.js 应用是开发模式还是生产模式

File diff suppressed because it is too large
+ 19488 - 1
package-lock.json


+ 3 - 0
package.json

@@ -19,6 +19,7 @@
     "@vant/touch-emulator": "^1.3.2",
     "clipboard": "^2.0.10",
     "core-js": "^3.19.3",
+    "element-ui": "^2.15.9",
     "js-cookie": "^3.0.1",
     "nuxt": "^2.15.8",
     "swiper": "^5.4.5",
@@ -30,7 +31,9 @@
     "webpack": "^4.46.0"
   },
   "devDependencies": {
+    "@nuxtjs/style-resources": "^1.2.1",
     "@nuxtjs/tailwindcss": "^4.2.1",
+    "babel-plugin-component": "^1.1.1",
     "cross-env": "^7.0.3",
     "eslint-config-prettier": "^8.3.0",
     "node-sass": "^6.0.1",

+ 43 - 26
pages/_template/app/approve/club/detail.vue

@@ -191,11 +191,9 @@ export default {
     overflow-y: auto;
     .club-info {
       padding: 32px 24px;
-      background: linear-gradient(
-        180deg,
-        #ffe6e8 0%,
-        rgba(255, 255, 255, 0) 100%
-      );
+      @include themify($themes) {
+        background: themed('cover-color');
+      }
 
       .info {
         width: 320px;
@@ -228,12 +226,16 @@ export default {
         }
         .mobile {
           &::after {
-            background-image: url(https://static.caimei365.com/www/authentic/pc/icon-phone-active.png);
+            @include themify($themes) {
+              background-image: themed('icon-phone-active-pc');
+            }
           }
         }
         .address {
           &::after {
-            background-image: url(https://static.caimei365.com/www/authentic/pc/icon-address-active.png);
+            @include themify($themes) {
+              background-image: themed('icon-address-active-pc');
+            }
           }
         }
       }
@@ -264,11 +266,14 @@ export default {
         align-items: center;
         width: 72px;
         height: 32px;
-        border: 1px solid #bc1724;
         border-radius: 4px;
-        background-color: #ffe6e8;
+
         font-size: 16px;
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+          border: 1px solid themed('color');
+          background-color: themed('sub-color');
+        }
         cursor: pointer;
 
         &::after {
@@ -276,8 +281,10 @@ export default {
           display: block;
           width: 16px;
           height: 16px;
-          background: url(https://static.caimei365.com/www/authentic/pc/icon-arround-right.png)
-            no-repeat center;
+          margin-left: 4px;
+          @include themify($themes) {
+            background: themed('icon-navigation-pc') no-repeat center;
+          }
           background-size: 16px;
         }
       }
@@ -320,10 +327,12 @@ export default {
           align-items: center;
           width: 80px;
           height: 32px;
-          background: #bc1724;
           border-radius: 4px;
           font-size: 14px;
           color: #ffffff;
+          @include themify($themes) {
+            background: themed('color');
+          }
         }
       }
     }
@@ -356,11 +365,9 @@ export default {
     }
     .club-info {
       padding: 4vw;
-      background: linear-gradient(
-        180deg,
-        #ffe6e8 0%,
-        rgba(255, 255, 255, 0) 100%
-      );
+      @include themify($themes) {
+        background: themed('cover-color');
+      }
 
       .info {
         width: 67vw;
@@ -393,12 +400,16 @@ export default {
         }
         .mobile {
           &::after {
-            background-image: url(https://static.caimei365.com/www/authentic/h5/icon-phone-active.png);
+            @include themify($themes) {
+              background-image: themed('icon-phone-active-h5');
+            }
           }
         }
         .address {
           &::after {
-            background-image: url(https://static.caimei365.com/www/authentic/h5/icon-address-active.png);
+            @include themify($themes) {
+              background-image: themed('icon-address-active-h5');
+            }
           }
         }
       }
@@ -429,19 +440,23 @@ export default {
         align-items: center;
         width: 14.4vw;
         height: 6.4vw;
-        border: 0.1vw solid #bc1724;
         border-radius: 0.4vw;
-        background-color: #ffe6e8;
         font-size: 3.2vw;
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+          border: 1px solid themed('color');
+          background-color: themed('sub-color');
+        }
 
         &::after {
           content: '';
           display: block;
           width: 3.6vw;
           height: 3.6vw;
-          background: url(https://static.caimei365.com/www/authentic/h5/icon-arround-right.png)
-            no-repeat center;
+          margin-left: 0.4vw;
+          @include themify($themes) {
+            background: themed('icon-navigation-pc') no-repeat center;
+          }
           background-size: 3.6vw;
         }
       }
@@ -480,10 +495,12 @@ export default {
           align-items: center;
           width: 15.8vw;
           height: 6.4vw;
-          background: #bc1724;
           border-radius: 0.4vw;
           font-size: 3vw;
           color: #ffffff;
+          @include themify($themes) {
+            background: themed('color');
+          }
         }
       }
     }

+ 16 - 6
pages/_template/app/approve/club/index.vue

@@ -251,8 +251,11 @@ export default {
   }
   .page-top {
     height: 420px;
-    background: url(https://static.caimei365.com/www/authentic/pc/bg-club.png);
-    background-size: auto 420px;
+    @include themify($themes) {
+      background: themed('banner-club-pc');
+      background-size: auto 420px;
+    }
+
     .logo {
       display: block;
       width: 120px;
@@ -293,7 +296,9 @@ export default {
       color: #404040;
 
       span {
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 
@@ -390,8 +395,11 @@ export default {
 @media screen and (max-width: 768px) {
   .page-top {
     height: 46vw;
-    background: url(https://static.caimei365.com/www/authentic/h5/bg-club.png);
-    background-size: auto 46vw;
+    @include themify($themes) {
+      background: themed('banner-club-pc');
+      background-size: auto 46vw;
+    }
+
     .logo {
       display: block;
       width: 14.8vw;
@@ -424,7 +432,9 @@ export default {
       color: #404040;
 
       span {
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 

+ 15 - 6
pages/_template/app/approve/device/index.vue

@@ -129,8 +129,11 @@ export default {
   }
   .page-top {
     height: 420px;
-    background: url(https://static.caimei365.com/www/authentic/pc/bg-device.png);
-    background-size: auto 420px;
+    @include themify($themes) {
+      background: themed('banner-device-pc');
+      background-size: auto 420px;
+    }
+
     .logo {
       display: block;
       width: 120px;
@@ -164,7 +167,9 @@ export default {
       color: #404040;
 
       span {
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 
@@ -215,8 +220,10 @@ export default {
 @media screen and (max-width: 768px) {
   .page-top {
     height: 46vw;
-    background: url(https://static.caimei365.com/www/authentic/h5/bg-device.png);
-    background-size: auto 46vw;
+    @include themify($themes) {
+      background: themed('banner-device-h5');
+      background-size: auto 46vw;
+    }
     .logo {
       display: block;
       width: 14.8vw;
@@ -243,7 +250,9 @@ export default {
       color: #404040;
 
       span {
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 

+ 16 - 6
pages/_template/app/approve/device/list.vue

@@ -153,8 +153,11 @@ export default {
   }
   .page-top {
     height: 420px;
-    background: url(https://static.caimei365.com/www/authentic/pc/bg-device.png);
-    background-size: auto 420px;
+    @include themify($themes) {
+      background: themed('banner-device-pc');
+      background-size: auto 420px;
+    }
+
     .logo {
       display: block;
       width: 120px;
@@ -188,7 +191,9 @@ export default {
       color: #404040;
 
       span {
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 
@@ -256,8 +261,11 @@ export default {
 @media screen and (max-width: 768px) {
   .page-top {
     height: 46vw;
-    background: url(https://static.caimei365.com/www/authentic/h5/bg-club.png);
-    background-size: auto 46vw;
+    @include themify($themes) {
+      background: themed('banner-device-h5');
+      background-size: auto 46vw;
+    }
+
     .logo {
       display: block;
       width: 14.8vw;
@@ -284,7 +292,9 @@ export default {
       color: #404040;
 
       span {
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 

+ 15 - 7
pages/_template/app/approve/index.vue

@@ -2,7 +2,7 @@
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
       <img class="logo" :src="supplierInfo.logo" />
-      <div class="name mt-2" v-text="supplierInfo.shopName"></div>
+      <div class="name mt-2" v-text="supplierInfo.shopName + '正品授权认证'"></div>
     </div>
     <div class="page-content">
       <div class="list">
@@ -75,8 +75,10 @@ export default {
   }
   .page-top {
     height: 360px;
-    background: url(https://static.caimei365.com/www/authentic/pc/bg-approve.png);
-    background-size: auto 360px;
+    @include themify($themes) {
+      background: themed('banner-approve-pc');
+      background-size: auto 360px;
+    }
     .logo {
       display: block;
       width: 120px;
@@ -115,7 +117,9 @@ export default {
       .line {
         width: 24px;
         height: 2px;
-        background-color: #bc1724;
+        @include themify($themes) {
+          background-color: themed('color');
+        }
       }
       .name {
         font-size: 20px;
@@ -139,8 +143,10 @@ export default {
 @media screen and (max-width: 768px) {
   .page-top {
     height: 46vw;
-    background: url(https://static.caimei365.com/www/authentic/h5/bg-approve.png);
-    background-size: auto 46vw;
+    @include themify($themes) {
+      background: themed('banner-approve-h5');
+      background-size: auto 46vw;
+    }
     .logo {
       display: block;
       width: 14.8vw;
@@ -167,7 +173,9 @@ export default {
       .line {
         width: 4.2vw;
         height: 0.3vw;
-        background-color: #bc1724;
+        @include themify($themes) {
+          background-color: themed('color');
+        }
       }
       .name {
         font-size: 4.2vw;

+ 6 - 2
pages/_template/app/approve/personnel/operate/detail.vue

@@ -137,7 +137,9 @@ export default {
           display: inline-block;
           width: 76px;
           height: 28px;
-          background: url(https://static.caimei365.com/www/authentic/pc/icon-doctor-level.png);
+          @include themify($themes) {
+            background: themed('icon-doctor-level-pc') no-repeat;
+          }
           background-size: 76px 28px;
           vertical-align: -5px;
           margin-left: 8px;
@@ -249,7 +251,9 @@ export default {
           display: inline-block;
           width: 13.6vw;
           height: 5vw;
-          background: url(https://static.caimei365.com/www/authentic/h5/icon-doctor-level.png);
+          @include themify($themes) {
+            background: themed('icon-doctor-level-h5') no-repeat;
+          }
           background-size: 13.6vw 5vw;
           vertical-align: -0.8vw;
           margin-left: 1.2vw;

+ 16 - 7
pages/_template/app/approve/personnel/operate/index.vue

@@ -137,9 +137,11 @@ export default {
 @media screen and (min-width: 768px) {
   .page-top {
     height: 420px;
-    background: url(https://static.caimei365.com/www/authentic/pc/bg-doctor.png)
-      no-repeat center;
-    background-size: auto 420px;
+    @include themify($themes) {
+      background: themed('banner-personnel-operate-pc') no-repeat center;
+      background-size: auto 420px;
+    }
+
     .logo {
       display: block;
       width: 120px;
@@ -173,7 +175,9 @@ export default {
       color: #404040;
 
       span {
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 
@@ -247,8 +251,11 @@ export default {
 @media screen and (max-width: 768px) {
   .page-top {
     height: 46vw;
-    background: url(https://static.caimei365.com/www/authentic/h5/bg-doctor.png);
-    background-size: auto 46vw;
+    @include themify($themes) {
+      background: themed('banner-personnel-operate-h5');
+      background-size: auto 46vw;
+    }
+
     .logo {
       display: block;
       width: 14.8vw;
@@ -279,7 +286,9 @@ export default {
       color: #404040;
 
       span {
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 

+ 4 - 1
pages/_template/app/database/article.vue

@@ -115,6 +115,7 @@ export default {
     },
     // 搜索
     onSearch(keyword) {
+      this.list = []
       this.listQuery.articleTitle = keyword
       this.listQuery.pageNum = 1
       this.fetchList()
@@ -185,7 +186,9 @@ export default {
 
       &:hover {
         .name {
-          color: #bc1724 !important;
+          @include themify($themes) {
+            color: themed('color') !important;
+          }
         }
       }
 

+ 10 - 3
pages/_template/app/database/file.vue

@@ -122,6 +122,7 @@ export default {
     },
     // 搜索
     onSearch(keyword) {
+      this.list = []
       this.listQuery.fileTitle = keyword
       this.listQuery.pageNum = 1
       this.fetchList()
@@ -196,7 +197,9 @@ export default {
 
       &:hover {
         .name {
-          color: #bc1724 !important;
+          @include themify($themes) {
+            color: themed('color') !important;
+          }
         }
       }
 
@@ -226,7 +229,9 @@ export default {
           top: 50%;
           transform: translateY(-50%);
           font-size: 16px;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           cursor: pointer;
 
           &::before {
@@ -314,7 +319,9 @@ export default {
           right: 0;
           bottom: 2.4vw;
           font-size: 3.2vw;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           cursor: pointer;
           margin-top: 1.2vw;
         }

+ 7 - 2
pages/_template/app/database/image.vue

@@ -135,6 +135,7 @@ export default {
     },
     // 搜索
     onSearch(keyword) {
+      this.list = []
       this.listQuery.imageTitle = keyword
       this.listQuery.pageNum = 1
       this.fetchList()
@@ -224,7 +225,9 @@ export default {
           bottom: 0;
           right: 0;
           position: absolute;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           font-size: 16px;
           cursor: pointer;
         }
@@ -317,7 +320,9 @@ export default {
           bottom: 2.4vw;
           right: 0;
           position: absolute;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           font-size: 3.6vw;
           cursor: pointer;
         }

+ 7 - 2
pages/_template/app/database/package.vue

@@ -118,6 +118,7 @@ export default {
     },
     // 搜索
     onSearch(keyword) {
+      this.list = []
       this.listQuery.fileTitle = keyword
       this.listQuery.pageNum = 1
       this.fetchList()
@@ -206,7 +207,9 @@ export default {
           top: 50%;
           transform: translateY(-50%);
           font-size: 16px;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           cursor: pointer;
           &::after {
             content: '>';
@@ -280,7 +283,9 @@ export default {
           bottom: 50%;
           transform: translateY(50%);
           font-size: 3.2vw;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           cursor: pointer;
 
           &::after {

+ 7 - 2
pages/_template/app/database/video.vue

@@ -130,6 +130,7 @@ export default {
     },
     // 搜索
     onSearch(keyword) {
+      this.list = []
       this.listQuery.videoTitle = keyword
       this.listQuery.pageNum = 1
       this.fetchList()
@@ -221,7 +222,9 @@ export default {
         }
         .download {
           font-size: 16px;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           cursor: pointer;
           margin-top: 8px;
         }
@@ -321,7 +324,9 @@ export default {
           right: 0;
           bottom: 2.4vw;
           font-size: 3.2vw;
-          color: #bc1724;
+          @include themify($themes) {
+            color: themed('color');
+          }
           cursor: pointer;
           margin-top: 1.2vw;
         }

+ 6 - 2
pages/_template/app/feedback/index.vue

@@ -111,7 +111,9 @@ export default {
       margin-right: auto;
       border-radius: 4px;
       font-size: 16px;
-      background-color: #bc1724;
+      @include themify($themes) {
+        background-color: themed('color');
+      }
       color: #fff;
       transition: all 0.2s;
       cursor: pointer;
@@ -203,7 +205,9 @@ export default {
       height: 11.6vw;
       border-radius: 0.2vw;
       font-size: 4vw;
-      background-color: #bc1724;
+      @include themify($themes) {
+        background-color: themed('color');
+      }
       color: #fff;
 
       &.disabled {

+ 527 - 0
pages/_template/app/form/club-register.vue

@@ -0,0 +1,527 @@
+<template>
+  <div class="page">
+    <div class="page-top flex flex-col justify-center items-center">
+      <img class="logo" :src="supplierInfo.logo" />
+      <div
+        class="name mt-2"
+        v-text="supplierInfo.shopName + '正品授权申请'"
+      ></div>
+    </div>
+    <div class="page-content" v-if="!isRequest">
+      <template>
+        <!-- 进步条 -->
+        <SimpleStep :list="stepList" :active="step" v-if="showStepBar" />
+        <div class="step-list py-4">
+          <!-- 账号注册表单 -->
+          <keep-alive>
+            <FormClubRegister
+              v-if="step === 1"
+              ref="userForm"
+              @step="onUserFormStep"
+            />
+          </keep-alive>
+          <!-- 机构认证表单 -->
+          <keep-alive>
+            <FormClubInfo
+              v-if="step === 2 && registerType.indexOf(2) !== -1"
+              ref="clubInfoForm"
+              @step="onClubInfoFormStep"
+            />
+          </keep-alive>
+          <!-- 设备认证表单 -->
+          <keep-alive>
+            <FormClubDevice
+              v-if="step === 3"
+              ref="clubDeviceForm"
+              @step="onclubDeviceFormStep"
+            />
+          </keep-alive>
+        </div>
+      </template>
+      <!-- 机构已认证 || 机构认证中 || 机构认证失败 -->
+      <template v-if="step === 2 && registerType.indexOf(2) === -1">
+        <div class="message">
+          <div class="status-icon" :class="autidStatusClass"></div>
+          <div class="status">
+            <span v-if="autidStatus === 0">机构认证失败</span>
+            <span v-if="autidStatus === 1">机构认证成功</span>
+            <span v-if="autidStatus === 2">机构认证中</span>
+          </div>
+          <div class="tip">提示:可点击认证记录看查看详情</div>
+        </div>
+      </template>
+
+      <!-- 操作 -->
+      <div class="control flex flex-col items-center">
+        <div
+          class="button next flex justify-center items-center mb-2"
+          @click="onNextStep"
+        >
+          {{ step === 3 ? '提交' : '下一步' }}
+        </div>
+        <div
+          class="button prev flex justify-center items-center"
+          @click="onPrevStep"
+          v-if="showPreButton"
+        >
+          上一步
+        </div>
+        <div class="record mt-2" @click="toRecord">认证记录</div>
+      </div>
+    </div>
+
+    <SimpleDialog
+      v-model="active"
+      @confirm="onConfirm"
+      :cancel="false"
+      description="抱歉,该用户已进行过正品授权申请"
+      :center="true"
+    />
+  </div>
+</template>
+
+<script>
+import SimpleStep from '@/components/SimpleStep'
+import FormClubRegister from './components/form-club-register.vue'
+import FormClubInfo from './components/form-club-info.vue'
+import FormClubDevice from './components/form-club-device.vue'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app',
+  components: {
+    SimpleStep,
+    FormClubRegister,
+    FormClubInfo,
+    FormClubDevice,
+  },
+
+  data() {
+    return {
+      isRequest: true,
+      active: false,
+      registerType: [3],
+      step: 1,
+      stepList: [
+        {
+          label: '账号注册',
+          id: 1,
+          recordRoute: '/record/club/detail',
+        },
+        {
+          label: '机构认证',
+          id: 2,
+          recordRoute: '/record/club/detail',
+          auditStatus: '',
+        },
+        {
+          label: '设备认证',
+          id: 3,
+          recordRoute: '/record/device',
+          auditStatus: '',
+        },
+      ],
+
+      // 机构用户信息
+      clubUserInfo: {},
+      // 机构授权信息
+      authInfo: {},
+      // 机构认证设备列表信息
+      productInfo: [],
+
+      // 机构授权id
+      authId: '',
+
+      autidStatus: 0,
+    }
+  },
+
+  computed: {
+    ...mapGetters([
+      'supplierInfo',
+      'authUserId',
+      'routePrefix',
+      'accessToken',
+      'userInfo',
+      'clubUserId',
+    ]),
+
+    autidStatusClass() {
+      if (this.autidStatus === 0) return 'danger'
+      if (this.autidStatus === 1) return 'success'
+      if (this.autidStatus === 2) return 'warning'
+    },
+
+    showStepBar() {
+      if (this.step === 2) {
+        if (this.registerType.indexOf(2) > -1) {
+          return true
+        } else {
+          return false
+        }
+      }
+      return true
+    },
+
+    showPreButton() {
+      if (this.step === 1) return false
+      if (this.step === 2) {
+        if (this.registerType.indexOf(1) > -1) {
+          return true
+        } else {
+          return false
+        }
+      }
+      return true
+    },
+  },
+  created() {
+    this.authId = this.$route.query.authId || ''
+    this.isRequest = true
+    this.initPageForm()
+  },
+  methods: {
+    onConfirm() {
+      this.$router.push(this.routePrefix)
+    },
+
+    async onNextStep() {
+      const validateAction = {
+        1: this.$refs.userForm?.validate,
+        2: this.$refs.clubInfoForm?.validate,
+        3: this.$refs.clubDeviceForm?.validate,
+      }
+      try {
+        // 表单校验
+        validateAction[this.step] && (await validateAction[this.step]())
+        // 提交
+        if (this.step === 3) {
+          this.onSubmit()
+        }
+        // 下一步
+        if (this.step < 3) {
+          this.step++
+        }
+      } catch (error) {
+        console.log(error)
+      }
+      console.log('userfomr', this.clubUserInfo)
+    },
+    onPrevStep() {
+      this.step > 1 && this.step--
+    },
+
+    async onSubmit() {
+      const params = {
+        registerType: this.registerType.join(','),
+        authUserId: this.authUserId,
+        authId: this.authId,
+        clubUserId: this.clubUserId,
+        clubUserInfo: this.clubUserInfo,
+        authInfo: this.authInfo,
+        productInfo: this.productInfo,
+      }
+
+      console.log(params)
+
+      try {
+        const res = await this.$http.api.clubUserRegisterAll(params)
+        console.log(res)
+        this.$router.push(`${this.routePrefix}/record/message`)
+      } catch (error) {
+        console.log(error)
+        this.$toast(error.msg)
+      }
+    },
+
+    onUserFormStep(data) {
+      console.log(data)
+      this.clubUserInfo = data
+    },
+
+    onClubInfoFormStep(data) {
+      console.log(data)
+      this.authInfo = data
+    },
+
+    onclubDeviceFormStep(data) {
+      console.log(data)
+      this.productInfo = data
+    },
+
+    toRecord() {
+      if (!this.accessToken) {
+        this.$toast('请登录后查看')
+        this.$router.push(`${this.routePrefix}`)
+        return
+      }
+      this.$router.push(`${this.routePrefix}/record/club/detail`)
+    },
+
+    // 初始化从页面进入的表单
+    async initFormWithLink() {
+      const authId = this.$route.query.authId
+      if (this.accessToken) {
+        // 已登录
+        this.step = 2
+        this.stepList = this.stepList.filter((item) => item.id !== 1)
+      } else {
+        this.registerType.push(1)
+      }
+
+      await this.initClubInfo({
+        authUserId: this.authUserId,
+        authId: authId,
+      })
+      this.isRequest = false
+    },
+
+    // 初始化从正常页面进入的表单
+    async initFormWithNormal() {
+      if (this.accessToken) {
+        // 已登录
+        this.step = 2
+        this.stepList = this.stepList.filter((item) => item.id !== 1)
+        await this.initClubInfo({
+          authUserId: this.authUserId,
+          mobile: this.userInfo.mobile,
+        })
+      } else {
+        // 未登录
+        this.registerType.push(1, 2)
+      }
+      this.isRequest = false
+    },
+
+    // 初始化表单
+    initPageForm() {
+      const linkType = this.$route.query.type || 'normal'
+      const taskMap = {
+        link: this.initFormWithLink,
+        normal: this.initFormWithNormal,
+      }
+      taskMap[linkType]()
+    },
+
+    // 判断用户手机号是否绑定机构
+    async initClubInfo(data) {
+      try {
+        const res = await this.$http.api.fetchClubAuthInfo(data)
+
+        if (res.data.auth) {
+          this.autidStatus = res.data.auth.auditStatus
+          this.authId = res.data.auth.authId
+        } else {
+          this.registerType.push(2)
+        }
+        return Promise.resolve(res)
+      } catch (error) {
+        console.log(error)
+        return Promise.reject(error)
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('banner-club-register');
+      background-size: auto 360px;
+    }
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1200px;
+    margin: 0 auto;
+    overflow: hidden;
+    min-height: calc(100vh - 80px - 80px - 360px);
+    box-sizing: border-box;
+    padding-bottom: 40px;
+
+    .message {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+      margin-top: 60px;
+      .status-icon {
+        width: 88px;
+        height: 88px;
+        background-repeat: no-repeat;
+        background-size: 75px auto;
+        background-position: center;
+
+        &.success {
+          background-image: url(https://static.caimei365.com/www/authentic/pc/icon-auth-primary.png);
+        }
+        &.warning {
+          background-image: url(https://static.caimei365.com/www/authentic/pc/icon-auth-warning.png);
+        }
+        &.danger {
+          background-image: url(https://static.caimei365.com/www/authentic/pc/icon-auth-danger.png);
+        }
+      }
+
+      .status {
+        font-size: 18px;
+        color: #282828;
+        margin: 12px 0;
+      }
+
+      .tip {
+        color: #999999;
+        font-size: 14px;
+      }
+    }
+
+    .control {
+      margin-top: 62px;
+      .button {
+        width: 295px;
+        height: 50px;
+
+        cursor: pointer;
+
+        &.prev {
+          @include themify($themes) {
+            border: 1px solid themed('color');
+            color: themed('color');
+          }
+        }
+        &.next {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+      .record {
+        font-size: 14px;
+        cursor: pointer;
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .step-list {
+      width: 700px;
+      margin: 0 auto;
+    }
+  }
+}
+
+// 移动端
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('banner-home-h5');
+      background-size: auto 46vw;
+    }
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+
+  .page-content {
+    padding: 0 7vw 7vw;
+
+    .message {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+      margin: 22.8vw 0;
+      .status-icon {
+        width: 23.6vw;
+        height: 23.6vw;
+        background-repeat: no-repeat;
+        background-size: 20vw auto;
+        background-position: center;
+
+        &.success {
+          background-image: url(https://static.caimei365.com/www/authentic/h5/icon-auth-primary.png);
+        }
+        &.warning {
+          background-image: url(https://static.caimei365.com/www/authentic/h5/icon-auth-warning.png);
+        }
+        &.danger {
+          background-image: url(https://static.caimei365.com/www/authentic/h5/icon-auth-danger.png);
+        }
+      }
+
+      .status {
+        font-size: 4.2vw;
+        color: #282828;
+        margin: 3.2vw 0 2.4vw;
+      }
+
+      .tip {
+        color: #999999;
+        font-size: 3.2vw;
+      }
+    }
+
+    .control {
+      .button {
+        width: 85.6vw;
+        height: 12vw;
+
+        cursor: pointer;
+
+        &.prev {
+          @include themify($themes) {
+            border: 1px solid themed('color');
+            color: themed('color');
+          }
+        }
+        &.next {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+      .record {
+        margin-top: 4.8vw;
+        font-size: 3.4vw;
+        cursor: pointer;
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+  }
+}
+</style>

+ 596 - 0
pages/_template/app/form/components/form-club-device.vue

@@ -0,0 +1,596 @@
+<template>
+  <div class="club-device">
+    <template v-for="formItem in formList">
+      <div :key="formItem.uid" class="device-section">
+        <span
+          class="remove-btn"
+          @click="removeOne(formItem)"
+          v-if="formList.length > 1"
+          >删除这台设备</span
+        >
+        <el-form :model="formItem" :rules="rules" ref="form">
+          <el-form-item prop="productName" :label="`设备名称${formItem.uuid}:`">
+            <el-select
+              v-model="formItem.productName"
+              filterable
+              allow-create
+              placeholder="请输入新设备名称或选择已有设备"
+              @change="onProductNameChange(formItem, $event)"
+            >
+              <el-option
+                v-for="item in deviceList"
+                :key="item.productTypeId"
+                :label="item.name"
+                :value="item.productTypeId"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item prop="productImage" label="设备图片:">
+            <br />
+            <el-input v-show="false" v-model="formItem.productImage"></el-input>
+            <SimpleUploadImage
+              :disabled="Boolean(formItem.productTypeId)"
+              :limit="1"
+              :image-list="formItem.productImageList"
+              :before-upload="beforeProductImageUpload"
+              @success="uploadProductImageSuccess(formItem, $event)"
+              @remove="handleProductImageRemove(formItem, $event)"
+            />
+          </el-form-item>
+          <el-form-item label="所属品牌:" prop="brandId">
+            <el-select v-model="formItem.brandId" placeholder="请选择品牌">
+              <el-option
+                v-for="item in brandList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item prop="purchaseWay" label="购买渠道:">
+            <el-input
+              placeholder="请输入购买渠道"
+              v-model="formItem.purchaseWay"
+            ></el-input>
+          </el-form-item>
+          <el-form-item prop="invoiceImage" label="发票:">
+            <br />
+            <el-input v-show="false" v-model="formItem.invoiceImage"></el-input>
+            <SimpleUploadImage
+              :limit="1"
+              :image-list="formItem.invoiceImageList"
+              :before-upload="beforeInvoiceImageUpload"
+              @success="uploadInvoiceImageSuccess(formItem, $event)"
+              @remove="handleInvoiceImageRemove(formItem, $event)"
+            />
+          </el-form-item>
+          <el-form-item prop="snCode" label="设备SN码:">
+            <el-input
+              placeholder="请输入设备SN码"
+              v-model="formItem.snCode"
+            ></el-input>
+          </el-form-item>
+          <el-form-item prop="paramList" label="设备参数:">
+            <br />
+            <div class="device-param-list">
+              <span class="add-param" @click="insertParam(formItem)"
+                >添加参数</span
+              >
+              <template v-for="(param, index) in formItem.paramList">
+                <div :key="index">
+                  <div class="param flex justify-between mb-4">
+                    <el-input
+                      style="width: 40%"
+                      placeholder="例如:品牌"
+                      class="mr-2"
+                      v-model="param.paramName"
+                    ></el-input>
+                    <el-input
+                      placeholder="请输入参数信息"
+                      v-model="param.paramContent"
+                    ></el-input>
+                    <span
+                      class="remove el-icon-close"
+                      @click="removeParam(formItem, index)"
+                      v-if="formItem.paramList.length > 4"
+                    ></span>
+                  </div>
+                </div>
+              </template>
+            </div>
+          </el-form-item>
+        </el-form>
+        <el-divider></el-divider>
+      </div>
+    </template>
+
+    <div class="add-device" @click="insertOne" v-if="formType !== 'edit'">
+      <div class="add-icon"></div>
+      添加设备
+    </div>
+
+    <SimpleDialog
+      v-if="formType !== 'edit'"
+      v-model="active"
+      @confirm="active = false"
+      confirmText="好的"
+      :cancel="false"
+      description="请慎重填写设备信息,认证通过后将无法更改!"
+      :center="true"
+    />
+  </div>
+</template>
+
+<script>
+import SimpleUploadImage from '@/components/SimpleUploadImage'
+import { mapGetters } from 'vuex'
+export default {
+  components: {
+    SimpleUploadImage,
+  },
+  props: {
+    formType: {
+      type: String,
+      default: 'add',
+    },
+  },
+  data() {
+    const productNameValidate = (rule, value, callback) => {
+      if (value.toString().length > 50) {
+        callback(new Error('设备名称长度需要在50个字符内'))
+      } else {
+        callback()
+      }
+    }
+
+    const paramListValidate = (rule, value, callback) => {
+      console.log(value)
+      const valid = value.some(
+        (item) =>
+          (item.paramName && !item.paramContent) ||
+          (!item.paramName && item.paramContent)
+      )
+
+      const isEmpty = value.every(
+        (item) => !item.paramName && !item.paramContent
+      )
+      if (valid || isEmpty) {
+        callback(new Error('参数列表不能为空'))
+      } else {
+        callback()
+      }
+    }
+
+    return {
+      active: true,
+      uuid: 0, // 表单id
+      productImageList: [],
+      rules: {
+        productName: [
+          { required: true, message: '设备名称不能为空', trigger: ['change'] },
+          { validator: productNameValidate, trigger: ['change'] },
+        ],
+        productImage: [
+          { required: true, message: '设备图片不能为空', trigger: ['change'] },
+        ],
+        brandId: [
+          { required: true, message: '所属品牌不能为空', trigger: ['change'] },
+        ],
+        snCode: [
+          { required: true, message: '设备SN码不能为空', trigger: ['blur'] },
+        ],
+        paramList: [
+          { required: true, message: '参数不能为空', trigger: ['blur'] },
+          { validator: paramListValidate, trigger: ['change'] },
+        ],
+        purchaseWay: [
+          {
+            required: true,
+            message: '请输入购买渠道不能为空',
+            trigger: ['blur'],
+          },
+          {
+            max: 50,
+            message: '最大长度为50个字符',
+            trigger: ['blur'],
+          },
+        ],
+        invoiceImage: [
+          { required: true, message: '请上传发票', trigger: ['change'] },
+        ],
+      },
+      formList: [],
+      brandList: [],
+      deviceList: [],
+    }
+  },
+
+  computed: {
+    ...mapGetters(['authUserId']),
+  },
+
+  created() {
+    this.fetchBrandList()
+    this.fetchDeviceList()
+    this.initFormList()
+  },
+
+  methods: {
+    // 表单验证
+    validate() {
+      this.$emit('step', this.formatFormList())
+      return Promise.all(this.$refs.form.map((item) => item.validate()))
+    },
+    async init(formData) {
+      console.log('formData', formData)
+      const obj = {}
+      const productImageList = [
+        {
+          name: '',
+          url: formData?.productImage,
+        },
+      ]
+      const invoiceImageList = [
+        {
+          name: '',
+          url: formData.invoiceImage,
+        },
+      ]
+      obj.uuid = ++this.uuid
+      obj.productImageList = productImageList
+      obj.invoiceImageList = invoiceImageList
+      obj.productImage = formData.productImage
+      obj.productName = formData.productName
+      obj.snCode = formData.snCode
+      obj.brandId = formData.brandId
+      obj.productId = formData.productId
+      obj.productTypeId = formData.productTypeId
+      obj.purchaseWay = formData.purchaseWay
+      obj.invoiceImage = formData.invoiceImage
+      obj.paramList = formData.paramList
+      this.formList.splice(0, 1, obj)
+      console.log('formList', this.formList)
+    },
+    formatFormList() {
+      const list = []
+      this.formList.forEach((formItem) => {
+        const obj = {}
+        obj.productImage = formItem.productImage
+        obj.productName = formItem.productName
+        obj.snCode = formItem.snCode
+        obj.brandId = formItem.brandId
+        obj.productId = formItem.productId
+        obj.source = 2
+        obj.productTypeId = formItem.productTypeId
+        obj.purchaseWay = formItem.purchaseWay
+        obj.invoiceImage = formItem.invoiceImage
+        obj.paramList = formItem.paramList
+        list.push(obj)
+      })
+      return list
+    },
+
+    generateFormData() {
+      return {
+        uuid: ++this.uuid,
+        authUserId: '',
+        authId: '', //	授权id
+        createBy: '', //	创建人id
+        // 	设备参数列表
+        paramList: this.initParams(),
+        productId: '', //	授权设备id
+        productImage: '', //	设备图片
+        productName: '', //	设备名称
+        snCode: '', //	设备SN码
+        brandId: '',
+        productTypeId: '',
+        purchaseWay: '', // 购买渠道
+        invoiceImage: '', // 发票
+        productImageList: [],
+        invoiceImageList: [],
+      }
+    },
+
+    generageProductParam() {
+      return {
+        paramContent: '',
+        paramName: '',
+      }
+    },
+
+    initParams() {
+      const list = []
+      for (let i = 0; i < 4; i++) {
+        list.push(this.generageProductParam())
+      }
+      return list
+    },
+
+    insertParam(formItem) {
+      formItem.paramList.push(this.generateFormData())
+    },
+
+    removeParam(formItem, index) {
+      formItem.paramList.splice(index, 1)
+    },
+
+    initFormList() {
+      this.formList.push(this.generateFormData())
+      console.log(this.formList)
+    },
+    insertOne() {
+      this.formList.push(this.generateFormData())
+    },
+    removeOne(formItem) {
+      const index = this.formList.findIndex(
+        (item) => item.uuid === formItem.uuid
+      )
+      this.formList.splice(index, 1)
+    },
+    onProductNameChange(formItem, value) {
+      if (typeof value === 'number') {
+        formItem.productTypeId = value
+        const deviceInfo = this.deviceList.find(
+          (item) => item.productTypeId === value
+        )
+        formItem.productImage = deviceInfo.image
+        formItem.productImageList = [{ name: '', url: deviceInfo.image }]
+      } else {
+        formItem.productTypeId = ''
+        formItem.productImage = ''
+        formItem.productImageList = []
+      }
+    },
+
+    // 获取品牌列表
+    async fetchBrandList() {
+      try {
+        const res = await this.$http.api.fetchBrandList({
+          type: 3,
+          authUserId: this.authUserId,
+        })
+        this.brandList = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 获取设备列表
+    async fetchDeviceList() {
+      try {
+        const res = await this.$http.api.fetchProductSelectList({
+          authUserId: this.authUserId,
+        })
+        this.deviceList = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 产品图片上传
+    beforeProductImageUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('上传产品图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+    uploadProductImageSuccess(formItem, { response, file, fileList }) {
+      formItem.productImageList = fileList
+      formItem.productImage = response.data
+    },
+    handleProductImageRemove(formItem, { file, fileList }) {
+      formItem.productImageList = fileList
+      formItem.productImage = ''
+    },
+
+    // 发票上传
+    beforeInvoiceImageUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('发票图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+    uploadInvoiceImageSuccess(formItem, { response, file, fileList }) {
+      formItem.invoiceImageList = fileList
+      formItem.invoiceImage = response.data
+    },
+    handleInvoiceImageRemove(formItem, { file, fileList }) {
+      formItem.invoiceImageList = fileList
+      formItem.invoiceImage = ''
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+// pc端
+@media screen and (min-width: 768px) {
+  .el-select {
+    width: 100%;
+  }
+
+  .device-section {
+    position: relative;
+
+    .el-form {
+      padding-bottom: 10px;
+    }
+
+    .remove-btn {
+      position: absolute;
+      right: 0;
+      bottom: 24px;
+      font-size: 16px;
+      color: #f94b4b;
+      text-decoration: underline;
+      cursor: pointer;
+    }
+  }
+
+  .device-param-list {
+    position: relative;
+    .add-param {
+      position: absolute;
+      cursor: pointer;
+      top: -40px;
+      right: 0;
+      text-decoration: underline;
+      font-size: 14px;
+      @include themify($themes) {
+        color: themed('color');
+      }
+    }
+
+    .param {
+      position: relative;
+      .remove {
+        position: absolute;
+        right: 0;
+        top: 0;
+        width: 20px;
+        height: 20px;
+        background: #f94b4b;
+        border-radius: 2px;
+        cursor: pointer;
+        color: #fff;
+        font-size: 14px;
+        text-align: center;
+        line-height: 20px;
+      }
+    }
+  }
+
+  .add-device {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 162px;
+    height: 46px;
+    border-radius: 4px;
+    box-sizing: border-box;
+    font-size: 18px;
+    margin: 0 auto;
+    cursor: pointer;
+
+    @include themify($themes) {
+      border: 1px solid themed('color');
+      color: themed('color');
+    }
+
+    .add-icon {
+      width: 20px;
+      height: 20px;
+      position: relative;
+      margin-right: 16px;
+
+      &::before,
+      &::after {
+        position: absolute;
+        width: 3px;
+        height: 20px;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        border-radius: 1px;
+        content: '';
+        display: block;
+        @include themify($themes) {
+          background: themed('color');
+        }
+      }
+      &::after {
+        transform: translate(-50%, -50%) rotateZ(90deg);
+      }
+    }
+  }
+}
+
+// 移动端
+@media screen and (max-width: 768px) {
+  ::v-deep {
+    .el-form-item__label {
+      font-size: 3.4vw;
+    }
+  }
+  .el-select {
+    width: 100%;
+  }
+  .device-param-list {
+    position: relative;
+    .add-param {
+      position: absolute;
+      cursor: pointer;
+      top: -40px;
+      right: 0;
+      font-size: 3.4vw;
+      @include themify($themes) {
+        color: themed('color');
+      }
+    }
+
+    .param {
+      position: relative;
+      .remove {
+        position: absolute;
+        right: 0;
+        top: 0;
+        width: 4.4vw;
+        height: 4.4vw;
+        background: #f94b4b;
+        border-radius: 0.2vw;
+        cursor: pointer;
+        color: #fff;
+        font-size: 3.4vw;
+        text-align: center;
+        line-height: 4.4vw;
+      }
+    }
+  }
+
+  .add-device {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 31vw;
+    height: 8.8vw;
+    border-radius: 0.4vw;
+    box-sizing: border-box;
+    font-size: 3.4vw;
+    margin: 0 auto;
+    cursor: pointer;
+
+    @include themify($themes) {
+      border: 1px solid themed('color');
+      color: themed('color');
+    }
+
+    .add-icon {
+      width: 20px;
+      height: 20px;
+      position: relative;
+      margin-right: 16px;
+
+      &::before,
+      &::after {
+        position: absolute;
+        width: 0.6vw;
+        height: 4.1vw;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        border-radius: 1px;
+        content: '';
+        display: block;
+        @include themify($themes) {
+          background: themed('color');
+        }
+      }
+      &::after {
+        transform: translate(-50%, -50%) rotateZ(90deg);
+      }
+    }
+  }
+}
+</style>

+ 806 - 0
pages/_template/app/form/components/form-club-info.vue

@@ -0,0 +1,806 @@
+<template>
+  <div class="club-info">
+    <el-form :model="formData" :rules="rules" ref="form" label-position="left">
+      <el-form-item prop="name" label="机构名称:">
+        <el-input
+          placeholder="请输入机构名称"
+          v-model="formData.name"
+          maxlength="50"
+          show-word-limit
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="mobile" label="联系电话:">
+        <el-input
+          placeholder="请输入对外联系电话"
+          v-model="formData.mobile"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="address" label="所在地区:">
+        <br />
+        <input type="text" v-model="formData.address" v-show="false" />
+        <div class="flex items-center justify-between">
+          <el-select
+            placeholder="请选择"
+            v-model="formData.provinceId"
+            @change="onProvinceChange"
+          >
+            <template v-for="item in provinceList">
+              <el-option :label="item.name" :value="item.id" :key="item.id">
+              </el-option>
+            </template>
+          </el-select>
+          <el-select
+            placeholder="请选择"
+            v-model="formData.cityId"
+            @change="onCityChange"
+            class="mx-2"
+          >
+            <template v-for="item in cityList">
+              <el-option :label="item.name" :value="item.id" :key="item.id">
+              </el-option>
+            </template>
+          </el-select>
+          <el-select
+            placeholder="请选择"
+            v-model="formData.townId"
+            @change="onTownChange"
+          >
+            <template v-for="item in townList">
+              <el-option :label="item.name" :value="item.id" :key="item.id">
+              </el-option>
+            </template>
+          </el-select>
+        </div>
+        <el-input
+          class="mt-4"
+          type="textarea"
+          :rows="4"
+          v-model="formData.fullAddress"
+          @input="onFullAddressInput"
+          placeholder="建议您如实填写详细收货地址,例如:街道名称,门牌号码,楼层和房间号等信息"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="point" label="">
+        <div class="normal-row">
+          <div class="label">
+            <i>*</i>所在位置:<span
+              >(提示:打开地图,将定位图标移到具体位置)</span
+            >
+          </div>
+
+          <div class="postion-btn" @click="initMap">定位</div>
+        </div>
+        <el-input v-model="formData.point" disabled></el-input>
+      </el-form-item>
+      <el-form-item prop="logoImage" label="logo:">
+        <br />
+        <el-input v-show="false" v-model="formData.logoImage" />
+        <SimpleUploadImage
+          :limit="1"
+          :image-list="logoList"
+          :before-upload="beforeLogoUpload"
+          @success="uploadLogoSuccess"
+          @remove="handleLogoRemove"
+        />
+      </el-form-item>
+      <el-form-item prop="banner">
+        <div class="normal-row">
+          <div class="label"><i>*</i>门头照:<span>(可上传6张)</span></div>
+          <el-input v-show="false" v-model="formData.banner" />
+          <SimpleUploadImage
+            :limit="6"
+            :image-list="bannerList"
+            :before-upload="beforeBannerUpload"
+            @success="uploadBannerSuccess"
+            @remove="handleBannerRemove"
+          />
+        </div>
+      </el-form-item>
+
+      <el-form-item label="机构类型:" prop="firstClubType">
+        <!-- <el-radio-group v-model="formData.firstClubType">
+          <el-radio :label="1">医美</el-radio>
+          <el-radio :label="2">生美</el-radio>
+          <el-radio :label="3">项目公司</el-radio>
+          <el-radio :label="4">个人</el-radio>
+          <el-radio :label="5">其他</el-radio>
+        </el-radio-group> -->
+        <SimpleRadio v-model="formData.firstClubType" :list="clubTypeList" />
+      </el-form-item>
+
+      <el-form-item
+        v-if="formData.firstClubType === 1"
+        label="医美类型:"
+        prop="secondClubType"
+      >
+        <!-- <el-radio-group v-model="formData.secondClubType">
+          <el-radio :label="1">诊所</el-radio>
+          <el-radio :label="2">门诊</el-radio>
+          <el-radio :label="3">医院</el-radio>
+          <el-radio :label="4">其他</el-radio>
+        </el-radio-group> -->
+        <SimpleRadio
+          v-model="formData.secondClubType"
+          :list="medicalTypeList1"
+          type="rect"
+        />
+      </el-form-item>
+
+      <el-form-item
+        v-if="formData.firstClubType === 2"
+        label="生美类型:"
+        prop="secondClubType"
+      >
+        <!-- <el-radio-group v-model="formData.secondClubType">
+          <el-radio :label="5">美容院</el-radio>
+          <el-radio :label="6">养生馆</el-radio>
+          <el-radio :label="7">其他</el-radio>
+        </el-radio-group> -->
+        <SimpleRadio
+          v-model="formData.secondClubType"
+          :list="medicalTypeList2"
+          type="rect"
+        />
+      </el-form-item>
+
+      <el-form-item
+        label="医疗许可证:"
+        prop="medicalLicenseImage"
+        v-if="formData.firstClubType === 1"
+      >
+        <br />
+        <el-input v-show="false" v-model="formData.medicalLicenseImage" />
+        <SimpleUploadImage
+          :limit="1"
+          :image-list="licenseImageList"
+          :before-upload="beforeLicenseImageUpload"
+          @success="uploadLicenseImageSuccess"
+          @remove="handleLicenseImageRemove"
+        />
+      </el-form-item>
+
+      <el-form-item label="员工人数:" prop="empNum">
+        <el-input
+          v-model.number="formData.empNum"
+          placeholder="请输入员工人数"
+          clearable
+        />
+      </el-form-item>
+    </el-form>
+
+    <div class="position-select" v-if="mapVisiable">
+      <div class="position-select-container">
+        <SimpleAMap
+          @position="onPosition"
+          ref="aMap"
+          :lnglat="lnglat"
+          :address="fullAddress"
+        />
+        <div class="position-select-footer">
+          <div class="lnglat">当前经纬度:{{ lgnlatText }}</div>
+          <div
+            class="position-cancel postion-control"
+            @click="mapVisiable = false"
+          >
+            取消
+          </div>
+          <div
+            class="position-confirm postion-control"
+            @click="mapVisiable = false"
+          >
+            确定
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <SimpleDialog
+      v-model="active"
+      @confirm="active = false"
+      :cancel="false"
+      confirmText="好的"
+      description="请慎重填写机构信息,认证通过后将无法更改!"
+      :center="true"
+    />
+  </div>
+</template>
+
+<script>
+import SimpleUploadImage from '@/components/SimpleUploadImage'
+import SimpleRadio from '@/components/SimpleRadio'
+import { isPoint, isNumber } from '@/utils/validator'
+export default {
+  components: {
+    SimpleUploadImage,
+    SimpleRadio,
+  },
+  data() {
+    var validatePoint = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('经纬度坐标不能为空'))
+      } else {
+        if (isPoint(value)) {
+          callback()
+        } else {
+          callback(
+            new Error('经纬度坐标格式不正确,(例如:114.095294,22.536004)')
+          )
+        }
+      }
+    }
+
+    var validateMobile = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('联系方式不能为空'))
+      } else {
+        if (isNumber(value)) {
+          callback()
+        } else {
+          callback(new Error('联系方式格式不正确'))
+        }
+      }
+    }
+
+    var validateAddress = (rule, value, callback) => {
+      if (
+        !this.formData.provinceId ||
+        !this.formData.cityId ||
+        !this.formData.townId ||
+        !this.formData.fullAddress
+      ) {
+        callback(new Error('请输入完整的地址'))
+      } else {
+        callback()
+      }
+    }
+
+    return {
+      clubTypeList: [
+        { value: 1, name: '医美' },
+        { value: 2, name: '生美' },
+        { value: 3, name: '项目公司' },
+        { value: 4, name: '个人' },
+        { value: 5, name: '其他' },
+      ],
+      medicalTypeList1: [
+        { value: 1, name: '诊所' },
+        { value: 2, name: '门诊' },
+        { value: 3, name: '医院' },
+        { value: 4, name: '其他' },
+      ],
+      medicalTypeList2: [
+        { value: 5, name: '美容院' },
+        { value: 6, name: '养生馆' },
+        { value: 7, name: '其他' },
+      ],
+      active: true,
+      lnglat: null,
+      mapVisiable: false,
+      formData: {
+        name: '',
+        address: '',
+        fullAddress: '',
+        point: '',
+        mobile: '',
+        userMobile: '',
+        logoImage: '',
+        banner: '',
+        customFlag: 0,
+        remarks: '',
+        empNum: '',
+        firstClubType: 1,
+        secondClubType: 1,
+        medicalLicenseImage: '',
+        provinceId: '',
+        cityId: '',
+        townId: '',
+      },
+      rules: {
+        name: [
+          { required: true, message: '机构名称不能为空', trigger: ['blur'] },
+        ],
+        logoImage: [
+          { required: true, message: '请上传logo', trigger: ['change'] },
+        ],
+        mobile: [
+          {
+            required: true,
+            validator: validateMobile,
+            trigger: ['blur', 'change'],
+          },
+        ],
+        point: [
+          {
+            required: true,
+            validator: validatePoint,
+            trigger: ['change'],
+          },
+        ],
+        address: [
+          {
+            required: true,
+            message: '所在地区不能为空',
+            trigger: ['change'],
+          },
+          {
+            validator: validateAddress,
+            trigger: ['change'],
+          },
+        ],
+        banner: [
+          {
+            required: true,
+            message: '门头照不能为空',
+            trigger: ['change'],
+          },
+        ],
+        empNum: [
+          { required: true, message: '员工人数不能为空', trigger: ['blur'] },
+        ],
+        firstClubType: [
+          { required: true, message: '机构类型不能为空', trigger: ['change'] },
+        ],
+        secondClubType: [
+          {
+            required: true,
+            message: '医美类型/生美类型不能为空',
+            trigger: ['change'],
+          },
+        ],
+        medicalLicenseImage: [
+          {
+            required: true,
+            message: '医疗许可证不能为空',
+            trigger: ['change'],
+          },
+        ],
+      },
+      // logo图片列表
+      logoList: [],
+      // banner图片列表
+      bannerList: [],
+      // 级联选择的地址
+      address: '',
+      // 医疗许可证图片
+      licenseImageList: [],
+
+      provinceList: [],
+    }
+  },
+  computed: {
+    lgnlatText() {
+      return this.lnglat ? this.lnglat.join(',') : ''
+    },
+    cityList() {
+      const province = this.provinceList.find(
+        (item) => item.id === this.formData.provinceId
+      )
+      if (province) {
+        return province.children
+      }
+      return []
+    },
+    townList() {
+      const city = this.cityList.find(
+        (item) => item.id === this.formData.cityId
+      )
+      if (city) {
+        return city.children
+      }
+      return []
+    },
+
+    fullAddress() {
+      let str = ''
+      this.provinceList.forEach((pro) => {
+        if (pro.id === this.formData.provinceId) {
+          str += pro.name
+          pro.children.forEach((city) => {
+            if (city.id === this.formData.cityId) {
+              str += city.name
+              city.children.forEach((town) => {
+                if (town.id === this.formData.townId) {
+                  str += town.name
+                }
+              })
+            }
+          })
+        }
+      })
+      return (str += this.formData.fullAddress)
+    },
+  },
+  created() {
+    this.fetchAllCityList()
+  },
+  methods: {
+    // 地图定位
+    initMap() {
+      this.mapVisiable = true
+      this.$nextTick(() => {
+        this.$refs.aMap.init()
+      })
+    },
+
+    onPosition(lnglat) {
+      console.log(lnglat)
+      this.lnglat = [lnglat.lng, lnglat.lat]
+      this.formData.point = this.lnglat.join(',')
+    },
+
+    async fetchAllCityList() {
+      try {
+        const res = await this.$http.api.fetchAllCityList()
+        console.log(res)
+        this.provinceList = res.data
+        return res
+      } catch (error) {
+        console.log(error)
+        return Promise.reject(error)
+      }
+    },
+
+    genetageFormData() {
+      return {
+        authParty: this.formData.name,
+        provinceId: this.formData.provinceId,
+        cityId: this.formData.cityId,
+        townId: this.formData.townId,
+        address: this.formData.fullAddress,
+        mobile: this.formData.mobile,
+        logo: this.formData.logoImage,
+        lngAndLat: this.lgnlatText,
+        remarks: this.formData.remarks,
+        empNum: this.formData.empNum,
+        firstClubType: this.formData.firstClubType,
+        secondClubType: this.formData.secondClubType,
+        medicalLicenseImage: this.formData.medicalLicenseImage,
+        bannerList: this.bannerList.map((item) =>
+          item.response ? item.response.data : item.url
+        ),
+      }
+    },
+
+    async init(formData) {
+      this.formData.name = formData.authParty
+      this.formData.provinceId = formData.provinceId
+      this.formData.cityId = formData.cityId
+      this.formData.townId = formData.townId
+      this.formData.fullAddress = formData.address
+      this.formData.mobile = formData.mobile
+      this.formData.logoImage = formData.logo
+      this.formData.empNum = formData.empNum
+      this.formData.firstClubType = formData.firstClubType
+      this.formData.secondClubType = formData.secondClubType
+      this.formData.medicalLicenseImage = formData.medicalLicenseImage
+      this.formData.point = formData.lngAndLat
+      this.lnglat = formData.lngAndLat.split(',')
+      this.logoList = [{ name: '', url: formData.logo }]
+      this.bannerList = formData.bannerList.map((item) => ({
+        name: '',
+        url: item,
+      }))
+
+      if (formData.medicalLicenseImage) {
+        this.licenseImageList = [
+          { name: '', url: formData.medicalLicenseImage },
+        ]
+      }
+
+      this.countAddress()
+
+      this.formData.banner =
+        this.bannerList.length > 0 ? this.bannerList.length : ''
+    },
+
+    // 表单验证
+    validate() {
+      this.$emit('step', this.genetageFormData())
+      return this.$refs.form.validate()
+    },
+
+    onProvinceChange() {
+      this.formData.cityId = ''
+      this.formData.townId = ''
+      this.countAddress()
+    },
+    onCityChange() {
+      this.formData.townId = ''
+      this.countAddress()
+    },
+    onTownChange() {
+      this.countAddress()
+    },
+
+    onFullAddressInput() {
+      this.countAddress()
+    },
+
+    countAddress() {
+      this.formData.address =
+        this.formData.cityId +
+        this.formData.provinceId +
+        this.formData.townId +
+        this.formData.fullAddress
+    },
+
+    // logo上传
+    uploadLogoSuccess({ response, file, fileList }) {
+      this.logoList = fileList
+      this.formData.logoImage = fileList[0].response.data
+    },
+    handleLogoRemove({ file, fileList }) {
+      this.logoList = fileList
+      this.formData.logoImage = ''
+    },
+    beforeLogoUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('上传logo图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+
+    // banner上传
+    uploadBannerSuccess({ response, file, fileList }) {
+      this.bannerList = fileList
+      console.log(this.bannerList)
+      this.formData.banner = fileList.length > 0 ? fileList.length : ''
+    },
+    handleBannerRemove({ file, fileList }) {
+      this.bannerList = fileList
+      this.formData.banner = fileList.length > 0 ? fileList.length : ''
+    },
+    beforeBannerUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('上传banner图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+
+    // 医疗许可证图片上传
+    uploadLicenseImageSuccess({ response, file, fileList }) {
+      this.licenseImageList = fileList
+      console.log(this.licenseImageList)
+      this.formData.medicalLicenseImage = response.data
+    },
+    handleLicenseImageRemove({ file, fileList }) {
+      this.licenseImageList = fileList
+      this.formData.medicalLicenseImage = ''
+    },
+    beforeLicenseImageUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('医疗许可证图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+// pc端
+@media screen and (min-width: 768px) {
+  .position-select {
+    width: 100vw;
+    height: 100vh;
+    background: rgba(0, 0, 0, 0.39);
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 999;
+
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .position-select-container {
+      background: #fff;
+      width: 60%;
+      box-sizing: border-box;
+      padding: 24px;
+
+      .position-select-footer {
+        position: relative;
+        display: flex;
+        justify-content: flex-end;
+        align-items: center;
+        padding-top: 24px;
+
+        .lnglat {
+          position: absolute;
+          font-size: 14px;
+          color: #666;
+
+          left: 0;
+          top: 50%;
+          transform: translateY(-50%);
+        }
+      }
+
+      .postion-control {
+        width: 120px;
+        height: 40px;
+        font-size: 14px;
+        border-radius: 4px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+        margin-left: 16px;
+
+        &.position-confirm {
+          background: #f56c6c;
+          color: #fff;
+        }
+
+        &.position-cancel {
+          background: #b1b1b1;
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  .normal-row {
+    position: relative;
+    .label {
+      font-size: 14px;
+      color: #606266;
+
+      i {
+        color: #f56c6c;
+        margin-right: 4px;
+      }
+
+      span {
+        color: #b2b2b2;
+      }
+    }
+    .postion-btn {
+      position: absolute;
+      top: 50%;
+      right: 0;
+      transform: translateY(-50%);
+      width: 62px;
+      height: 28px;
+      line-height: 28px;
+      font-size: 14px;
+      color: #fff;
+      background: #1890ff;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      cursor: pointer;
+      border-radius: 4px;
+
+      &::before {
+        content: '';
+        display: inline-block;
+        width: 16px;
+        height: 16px;
+        background: url(https://static.caimei365.com/www/authentic/pc/icon-position.png)
+          no-repeat center;
+        background-size: 16px 16px;
+      }
+    }
+  }
+}
+
+// 移动端
+@media screen and (max-width: 768px) {
+  ::v-deep {
+    .el-form-item__label {
+      font-size: 3.4vw;
+    }
+  }
+
+  .position-select {
+    width: 100vw;
+    height: 100vh;
+    background: rgba(0, 0, 0, 0.39);
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 999;
+
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .position-select-container {
+      background: #fff;
+      width: 80%;
+      box-sizing: border-box;
+      padding: 3.2vw;
+
+      .position-select-footer {
+        padding-top: 10vw;
+        position: relative;
+        display: flex;
+        justify-content: flex-end;
+        align-items: center;
+
+        .lnglat {
+          position: absolute;
+          font-size: 3.2vw;
+          color: #666;
+
+          left: 0;
+          top: 5vw;
+          transform: translateY(-50%);
+        }
+      }
+
+      .postion-control {
+        width: 16vw;
+        height: 7vw;
+        font-size: 3.4vw;
+        border-radius: 0.4vw;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+        margin-left: 3.6vw;
+
+        &.position-confirm {
+          background: #f56c6c;
+          color: #fff;
+        }
+
+        &.position-cancel {
+          background: #b1b1b1;
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  .normal-row {
+    position: relative;
+    .label {
+      font-size: 14px;
+      color: #606266;
+
+      i {
+        color: #f56c6c;
+        margin-right: 4px;
+      }
+
+      span {
+        color: #b2b2b2;
+        font-size: 2.6vw;
+      }
+    }
+    .postion-btn {
+      position: absolute;
+      top: 50%;
+      right: 0;
+      transform: translateY(-50%);
+      width: 14vw;
+      height: 6.8vw;
+      line-height: 6.8vw;
+      font-size: 3.2vw;
+      color: #fff;
+      background: #1890ff;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      cursor: pointer;
+      border-radius: 0.4vw;
+
+      &::before {
+        content: '';
+        display: inline-block;
+        width: 3.58vw;
+        height: 3.58vw;
+        background: url(https://static.caimei365.com/www/authentic/pc/icon-position.png)
+          no-repeat center;
+        background-size: 3.58vw;
+      }
+    }
+  }
+}
+</style>

+ 221 - 0
pages/_template/app/form/components/form-club-register.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="club-register">
+    <el-form :model="formData" :rules="rules" ref="form" label-width="0">
+      <el-form-item prop="mobile">
+        <el-input
+          type="text"
+          v-model="formData.mobile"
+          placeholder="手机号"
+          @blur="onMobileBlur"
+          maxlength="11"
+          show-word-limit
+          @input="handleMobileInput"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="verifyCode">
+        <div class="verifyCode flex justify-between">
+          <el-input
+            v-model="formData.verifyCode"
+            placeholder="验证码"
+            maxlength="6"
+            show-word-limit
+            @input="handleVerifyCodeInput"
+          ></el-input>
+          <div class="send ml-8" @click="onSend">{{ sendCodeBtnText }}</div>
+        </div>
+      </el-form-item>
+      <el-form-item prop="password">
+        <el-input
+          type="password"
+          v-model="formData.password"
+          placeholder="密码"
+          maxlength="12"
+          show-word-limit
+          show-password
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="confirmPwd">
+        <el-input
+          type="password"
+          v-model="formData.confirmPwd"
+          placeholder="再次输入密码"
+          maxlength="12"
+          show-word-limit
+          show-password
+        ></el-input>
+      </el-form-item>
+    </el-form>
+
+    <SimpleDialog
+      v-model="active"
+      @confirm="onConfirm"
+      @cancel="onCancel"
+      confirmText="去登录"
+      description="抱歉,该手机号已注册,您可以登录后再来进行正品授权申请!"
+      :center="true"
+    />
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { isMobile } from '@/utils/validator'
+export default {
+  data() {
+    const confirmPwdValide = (rule, value, callback) => {
+      if (this.formData.password !== value) {
+        callback(new Error('两次输入的密码不一致'))
+      } else {
+        callback()
+      }
+    }
+
+    const mobileValidate = (rule, value, callback) => {
+      if (!isMobile(value)) {
+        callback(new Error('手机号格式不正确'))
+      } else {
+        callback()
+      }
+    }
+
+    return {
+      formData: {
+        mobile: '',
+        verifyCode: '',
+        password: '',
+        confirmPwd: '',
+      },
+      registerStatus: true, // 能否注册
+      sendStatus: 0,
+      active: false,
+      rules: {
+        mobile: [
+          { required: true, message: '手机号不能为空', trigger: ['blur'] },
+          { validator: mobileValidate, trigger: ['blur'] },
+        ],
+        verifyCode: [
+          { required: true, message: '验证码不能为空', trigger: ['blur'] },
+        ],
+        password: [
+          { required: true, message: '密码不能为空', trigger: ['blur'] },
+          { min: 8, max: 12, message: '请输入8-12位密码', trigger: ['blur'] },
+        ],
+        confirmPwd: [
+          { required: true, message: '请再次输入密码', trigger: ['blur'] },
+          { validator: confirmPwdValide, trigger: ['blur'] },
+        ],
+      },
+    }
+  },
+  computed: {
+    ...mapGetters(['authUserId', 'routePrefix', 'accessToken']),
+    sendCodeBtnText() {
+      return this.sendStatus === 0
+        ? '获取验证码'
+        : `再次发送${this.sendStatus}s`
+    },
+  },
+  methods: {
+    async onSend() {
+      if (!this.registerStatus) return (this.active = true)
+
+      if (this.sendStatus > 0) return
+      // 验证手机号是否合法
+      if (!isMobile(this.formData.mobile)) {
+        this.$toast('请输入正确的手机号')
+        return
+      }
+      try {
+        // 发送验证码
+        await this.$http.api.sendVerifyCode({
+          mobile: this.formData.mobile,
+          authUserId: this.authUserId,
+          type: 1,
+        })
+        this.$toast('验证码已发送')
+        // 开启倒计时
+        this.countdown()
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 输入框输入时
+    handleMobileInput() {
+      this.formData.mobile = this.formData.mobile.replace(/\D/gi, '')
+    },
+
+    // 输入框输入时
+    handleVerifyCodeInput() {
+      this.formData.verifyCode = this.formData.verifyCode.replace(/\D/gi, '')
+    },
+
+    countdown() {
+      this.sendStatus = 30
+      this.timer = setInterval(() => {
+        if (this.sendStatus === 0) {
+          clearInterval(this.timer)
+          return
+        }
+        this.sendStatus--
+      }, 1000)
+    },
+
+    onConfirm() {
+      this.$router.push(`${this.routePrefix}`)
+    },
+    onCancel() {
+      this.active = false
+    },
+
+    onMobileBlur() {
+      if (isMobile(this.formData.mobile)) {
+        this.checkouMobileBindClub()
+      }
+    },
+    // 判断用户手机号是否绑定机构
+    async checkouMobileBindClub() {
+      try {
+        const res = await this.$http.api.fetchClubAuthInfo({
+          authUserId: this.authUserId,
+          mobile: this.formData.mobile,
+        })
+        if (res.data.clubUser) {
+          this.active = true
+          this.registerStatus = false
+        } else {
+          this.registerStatus = true
+        }
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    genetageFormData() {
+      return {
+        mobile: this.formData.mobile,
+        verifyCode: this.formData.verifyCode,
+        password: this.formData.password,
+      }
+    },
+
+    // 表单验证
+    validate() {
+      this.$emit('step', this.genetageFormData())
+      return this.$refs.form.validate()
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.verifyCode {
+  .send {
+    cursor: pointer;
+    white-space: nowrap;
+    @include themify($themes) {
+      color: themed('color');
+    }
+  }
+}
+</style>

+ 271 - 0
pages/_template/app/form/link-register.vue

@@ -0,0 +1,271 @@
+<template>
+  <div class="page">
+    <div class="page-content link-register flex justify-center items-center">
+      <div class="link-register-section flex justify-center items-center">
+        <div class="content">
+          <div class="logo"><img :src="supplierInfo.logo" alt="" /></div>
+          <div class="message">
+            完成账号注册与设备认证信息后,将获得<span>{{
+              supplierInfo.shopName
+            }}</span
+            >授权牌匾制作及寄送
+          </div>
+          <div class="control">
+            <div
+              class="button"
+              @click="toRegister"
+              v-if="!isRequest && bindStatus === 0"
+            >
+              点击进入
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <SimpleDialog
+      v-model="dialogActive"
+      @confirm="onConfirm"
+      :cancel="false"
+      :description="dialogText"
+      :center="true"
+    />
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app',
+  data() {
+    return {
+      authId: '',
+      dialogActive: false,
+      isRequest: false,
+      bindStatus: 0,
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'supplierInfo',
+      'authUserId',
+      'routePrefix',
+      'accessToken',
+      'userInfo',
+    ]),
+    dialogText() {
+      return this.bindStatus === 1
+        ? '该链接认证信息已被账号注册!'
+        : '抱歉,当前登录手机号已绑定机构,您可以登录后再来进行正品授权申请!'
+    },
+
+    redirectLink() {
+      return `${this.routePrefix}/form/club-register?type=link&authId=${this.authId}`
+    },
+  },
+
+  created() {
+    this.authId = this.$route.query.authId
+    this.checkoutClubIsBind()
+  },
+
+  methods: {
+    // 跳转首页
+    onConfirm() {
+      this.$router.push(`${this.routePrefix}`)
+    },
+
+    // 判断机构是否已经被绑定
+    async checkoutClubIsBind() {
+      this.isRequest = true
+      try {
+        const res = await this.$http.api.fetchClubAuthInfo({
+          authUserId: this.authUserId,
+          authId: this.authId,
+        })
+        const auth = res.data.auth
+        if (auth) {
+          this.bindStatus = auth.bindStatus
+          this.dialogActive = auth.bindStatus === 1
+        }
+        this.isRequest = false
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 跳转注册页面
+    async toRegister() {
+      if (this.accessToken) {
+        try {
+          const res = await this.$http.api.fetchClubAuthInfo({
+            authUserId: this.authUserId,
+            mobile: this.userInfo.mobile,
+          })
+          if (res.data.auth) {
+            this.dialogActive = true
+          } else {
+            this.$router.push(this.redirectLink)
+          }
+        } catch (error) {
+          console.log(error)
+        }
+      } else {
+        // 未登录状态缓存
+        const bindFlag = this.$getStorage(this.routePrefix, 'bind-flag')
+        if (bindFlag) this.$removeStorage(this.routePrefix, 'bind-flag')
+        if (!bindFlag) {
+          this.$setStorage(
+            this.routePrefix,
+            'club-register-link',
+            this.$route.fullPath
+          )
+        }
+        this.$router.push(this.redirectLink)
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page-content {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 10000;
+    width: 100vw;
+    height: 100vh;
+    background: url(https://static.caimei365.com/www/authentic/pc/link-register-bg.png)
+      no-repeat center;
+
+    .link-register-section {
+      width: 1200px;
+      height: 530px;
+      background: url(https://static.caimei365.com/www/authentic/pc/link-register-section-bg.png)
+        no-repeat center;
+      .content {
+        width: 1030px;
+        height: 400px;
+        background: #fff;
+        background-image: url(https://static.caimei365.com/www/authentic/pc/link-register-icon.png);
+        background-repeat: no-repeat;
+        background-position: 580px center;
+        box-sizing: border-box;
+        padding-left: 70px;
+
+        .logo {
+          height: 40px;
+          width: auto;
+          margin-top: 48px;
+
+          img {
+            display: block;
+            height: 40px;
+          }
+        }
+
+        .message {
+          width: 360px;
+          font-size: 20px;
+          line-height: 36px;
+          color: #282828;
+          margin: 40px 0 76px;
+
+          span {
+            font-weight: bold;
+          }
+        }
+
+        .control {
+          .button {
+            width: 295px;
+            height: 50px;
+            background: #409eff;
+            border-radius: 4px;
+            text-align: center;
+            font-size: 18px;
+            line-height: 50px;
+            color: #fff;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page-content {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 10000;
+    width: 100vw;
+    height: 100vh;
+    background: url(https://static.caimei365.com/www/authentic/h5/link-register-bg.png)
+      no-repeat center;
+
+    .link-register-section {
+      width: 100vw;
+      height: 119.5vw;
+      background: url(https://static.caimei365.com/www/authentic/h5/link-register-section-bg.png)
+        no-repeat center;
+      .content {
+        width: 88.6vw;
+        height: 106.6vw;
+        background: #fff;
+        background-image: url(https://static.caimei365.com/www/authentic/h5/link-register-icon.png);
+        background-repeat: no-repeat;
+        background-position: center 32vw;
+        background-size: 62vw auto;
+        box-sizing: border-box;
+        padding-left: 8vw;
+        position: relative;
+
+        .logo {
+          height: 8vw;
+          width: auto;
+          margin-top: 6.4vw;
+
+          img {
+            display: block;
+            height: 8vw;
+          }
+        }
+
+        .message {
+          width: 72vw;
+          font-size: 4vw;
+          line-height: 7.2vw;
+          color: #282828;
+          margin-top: 4vw;
+
+          span {
+            font-weight: bold;
+          }
+        }
+
+        .control {
+          position: absolute;
+          bottom: 7.2vw;
+          left: 50%;
+          transform: translateX(-50%);
+          .button {
+            width: 62vw;
+            height: 8.8vw;
+            background: #409eff;
+            border-radius: 0.4vw;
+            text-align: center;
+            font-size: 3.6vw;
+            line-height: 8.8vw;
+            color: #fff;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 61 - 37
pages/_template/app/index.vue

@@ -40,49 +40,61 @@ export default {
       list: [],
     }
   },
-  asyncData() {
-    return {
-      list: [
+  computed: {
+    ...mapGetters([
+      'supplierInfo',
+      'authUserId',
+      'appId',
+      'routePrefix',
+      'accountType',
+      'themeName',
+    ]),
+  },
+  created() {
+    this.initEntryItems()
+  },
+  beforeDestroy() {
+    this.$removeStorage(this.routePrefix, 'login_redicret')
+  },
+  methods: {
+    // 初始化入口图标
+    initEntryItems() {
+      this.list = [
         {
           id: 0,
-          name: '授权认证',
-          image: 'icon-approve.png',
-          hover: 'icon-approve-active.png',
-          path: '/approve',
+          name: '正品授权申请入口',
+          image: `${this.themeName}-icon-edit.png`,
+          hover: `icon-edit-active.png`,
+          path: '/form/club-register',
           active: false,
         },
         {
           id: 1,
+          name: '正品授权',
+          image: `${this.themeName}-icon-approve.png`,
+          hover: `icon-approve-active.png`,
+          path: '/approve',
+          active: false,
+        },
+        {
+          id: 2,
           name: '资料库',
-          image: 'icon-doc.png',
-          hover: 'icon-doc-active.png',
+          image: `${this.themeName}-icon-doc.png`,
+          hover: `icon-doc-active.png`,
           path: '/database/article',
           active: false,
         },
         {
-          id: 2,
+          id: 3,
           name: '意见反馈',
-          image: 'icon-feedback.png',
-          hover: 'icon-feedback-active.png',
+          image: `${this.themeName}-icon-feedback.png`,
+          hover: `icon-feedback-active.png`,
           path: '/feedback',
           active: false,
         },
-      ],
-    }
-  },
-  computed: {
-    ...mapGetters([
-      'supplierInfo',
-      'authUserId',
-      'appId',
-      'routePrefix',
-      'accountType',
-    ]),
-  },
-  beforeDestroy() {
-    this.$removeStorage(this.routePrefix, 'login_redicret')
-  },
-  methods: {
+      ]
+    },
+
     toDetail(item) {
       const hasLogin = this.$store.getters.accessToken
       // 保存登录重定向路由
@@ -91,7 +103,7 @@ export default {
         'login_redicret',
         this.routePrefix + item.path
       )
-      if (item.id > 0 && !hasLogin) {
+      if (item.id > 1 && !hasLogin) {
         // 在微信浏览器中使用微信授权登录
         if (isWeChat() && this.appId && this.accountType === 2) {
           const payload = {
@@ -104,8 +116,14 @@ export default {
         this.$store.commit('app/SHOW_LOGIN')
         return
       }
-      const url = this.routePrefix + item.path
-      this.$router.push(url)
+      
+      if (item.id === 0) {
+        const url = this.routePrefix + item.path
+        this.$router.push(url)
+      } else {
+        const url = this.routePrefix + item.path
+        this.$router.push(url)
+      }
     },
     onMouseover(item) {
       const isPc = this.$store.getters.isPc
@@ -126,8 +144,10 @@ export default {
 @media screen and (min-width: 768px) {
   .page-top {
     height: 360px;
-    background: url(https://static.caimei365.com/www/authentic/pc/bg-home.png);
-    background-size: auto 360px;
+    @include themify($themes) {
+      background: themed('banner-home-pc');
+      background-size: auto 360px;
+    }
     .logo {
       display: block;
       width: 120px;
@@ -153,7 +173,7 @@ export default {
     }
 
     .section {
-      width: 380px;
+      width: 284px;
       height: 260px;
       margin-left: auto;
       margin-right: auto;
@@ -162,7 +182,9 @@ export default {
       cursor: pointer;
 
       &:hover {
-        background-color: #bc1724;
+        @include themify($themes) {
+          background-color: themed('color');
+        }
         transform: translateY(-10px);
 
         .name {
@@ -187,8 +209,10 @@ export default {
 @media screen and (max-width: 768px) {
   .page-top {
     height: 46vw;
-    background: url(https://static.caimei365.com/www/authentic/h5/bg-home.png);
-    background-size: auto 46vw;
+    @include themify($themes) {
+      background: themed('banner-home-h5');
+      background-size: auto 46vw;
+    }
     .logo {
       display: block;
       width: 14.8vw;

+ 399 - 0
pages/_template/app/record/club/detail.vue

@@ -0,0 +1,399 @@
+<template>
+  <div class="page">
+    <div class="page-top flex flex-col justify-center items-center">
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="name mt-2" v-text="supplierInfo.shopName + '认证记录'"></div>
+    </div>
+    <div class="page-content">
+      <template v-if="clubInfo">
+        <div class="page-title">机构认证</div>
+        <div class="row">
+          <div class="col">机构名称:</div>
+          <div class="col">{{ clubInfo.authParty }}</div>
+        </div>
+        <div class="row">
+          <div class="col">联系电话:</div>
+          <div class="col">{{ clubInfo.mobile }}</div>
+        </div>
+        <div class="row">
+          <div class="col">所在地区:</div>
+          <div class="col">{{ clubInfo.area }}</div>
+        </div>
+        <div class="row">
+          <div class="col">所在位置:</div>
+          <div class="col">{{ clubInfo.address }}</div>
+        </div>
+        <div class="row">
+          <div class="col max-width">logo:</div>
+          <div class="col">
+            <el-image
+              v-if="clubInfo.logo"
+              :src="clubInfo.logo"
+              :preview-src-list="[clubInfo.logo]"
+            >
+            </el-image>
+            <span v-else>暂无图片</span>
+          </div>
+        </div>
+        <div class="row">
+          <div class="col max-width">门头照:</div>
+          <div class="col">
+            <template
+              v-if="clubInfo.bannerList && clubInfo.bannerList.length > 0"
+            >
+              <template v-for="(image, index) in clubInfo.bannerList">
+                <el-image
+                  :key="index"
+                  :src="image"
+                  :preview-src-list="clubInfo.bannerList"
+                />
+              </template>
+            </template>
+            <span v-else>暂无图片</span>
+          </div>
+        </div>
+        <div class="row">
+          <div class="col">机构类型:</div>
+          <div class="col">
+            {{
+              ['医美', '生美', '项目公司', '个人', '其他'][
+                clubInfo.firstClubType - 1
+              ]
+            }}
+          </div>
+        </div>
+        <div
+          class="row"
+          v-if="clubInfo.firstClubType === 1 || clubInfo.firstClubType === 2"
+        >
+          <div class="col">医美类型:</div>
+          <div class="col">
+            {{
+              ['诊所', '门诊', '医院', '其他', '美容院', '养生馆', '其他'][
+                clubInfo.secondClubType - 1
+              ]
+            }}
+          </div>
+        </div>
+        <div class="row" v-if="clubInfo.firstClubType === 1">
+          <div class="col max-width">医疗许可证:</div>
+          <div class="col">
+            <el-image
+              v-if="clubInfo.medicalLicenseImage"
+              :src="clubInfo.medicalLicenseImage"
+              :preview-src-list="[clubInfo.medicalLicenseImage]"
+            />
+            <span v-else>暂无图片</span>
+          </div>
+        </div>
+        <div class="row">
+          <div class="col">员工人数:</div>
+          <div class="col">{{ clubInfo.empNum }}</div>
+        </div>
+        <div class="row">
+          <div class="col">状态:</div>
+          <div class="col">
+            <div class="status">
+              <span class="success" v-if="clubInfo.auditStatus === 1"
+                >认证成功</span
+              >
+              <span class="warning" v-else-if="clubInfo.auditStatus === 2"
+                >认证中</span
+              >
+              <span class="danger" v-else>认证失败</span>
+            </div>
+          </div>
+        </div>
+
+        <div class="row" v-if="clubInfo.auditStatus === 0">
+          <div class="col">原因:</div>
+          <div class="col">
+            {{ clubInfo.invalidReason ? clubInfo.invalidReason : '暂无' }}
+          </div>
+        </div>
+
+        <div class="control flex flex-col items-center">
+          <div
+            class="button edit flex justify-center items-center mb-2"
+            @click="onEdit"
+            v-if="clubInfo.auditStatus === 0"
+          >
+            编辑
+          </div>
+          <div
+            class="button search flex justify-center items-center"
+            @click="onToDeviceList"
+          >
+            查看认证设备
+          </div>
+        </div>
+      </template>
+      <template v-else>
+        <SimpleEmpty name="icon-club-empty.png" description="暂无机构认证~" />
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+import SimpleEmpty from '@/components/SimpleEmpty'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app',
+  components: {
+    SimpleEmpty,
+  },
+  data() {
+    return {
+      clubInfo: null,
+      authId: ''
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'routePrefix', 'clubUserId', 'userInfo']),
+  },
+  created() {
+    this.fetchAuthDetail()
+  },
+  methods: {
+    // 获取认证机构信息
+    async fetchAuthDetail() {
+      try {
+        const result = await this.$http.api.fetchClubAuthInfo({
+          clubUserId: this.clubUserId,
+        })
+
+        if(!result.data.auth) return
+
+        this.authId = result.data.auth.authId
+
+        const res = await this.$http.api.fetchClubAuthInfoData({
+          authId: result.data.auth.authId,
+        })
+        
+        this.clubInfo = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    onToDeviceList() {
+      this.$router.push(`${this.routePrefix}/record/device?authId=${this.authId}`)
+    },
+    onEdit() {
+      this.$router.push(`${this.routePrefix}/record/club/edit`)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('banner-club-register');
+      background-size: auto 360px;
+    }
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 600px;
+    margin: 0 auto;
+    overflow: hidden;
+    min-height: calc(100vh - 80px - 80px - 360px);
+    box-sizing: border-box;
+    padding-bottom: 40px;
+
+    .page-title {
+      font-size: 24px;
+      font-weight: bold;
+      text-align: center;
+      padding: 40px 0;
+    }
+
+    .row {
+      display: flex;
+      justify-content: flex-start;
+      align-items: flex-start;
+      font-size: 18px;
+      margin: 24px 0;
+
+      .status {
+        .success {
+          color: #f3920d !important;
+        }
+        .warning {
+          color: #1890ff !important;
+        }
+        .danger {
+          color: #f94b4b !important;
+        }
+      }
+
+      .col {
+        &:first-child {
+          white-space: nowrap;
+          width: 110px;
+          color: #666;
+          text-align: right;
+          flex-shrink: 0;
+        }
+
+        &:last-child {
+          color: #282828;
+        }
+      }
+
+      .el-image {
+        width: 120px;
+        height: 120px;
+        margin-right: 12px;
+      }
+    }
+
+    .control {
+      margin-top: 62px;
+      .button {
+        width: 295px;
+        height: 50px;
+
+        cursor: pointer;
+
+        &.edit {
+          @include themify($themes) {
+            border: 1px solid themed('color');
+            color: themed('color');
+          }
+        }
+
+        &.search {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('banner-home-h5');
+      background-size: auto 46vw;
+    }
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+
+  .page-content {
+    box-sizing: border-box;
+    padding: 8vw 7.2vw;
+
+    .page-title {
+      font-size: 4.2vw;
+      font-weight: bold;
+      text-align: center;
+      margin-bottom: 8vw;
+      color: #282828;
+    }
+
+    .row {
+      display: flex;
+      justify-content: flex-start;
+      align-items: flex-start;
+      font-size: 3.4vw;
+      margin-bottom: 5.6vw;
+      flex-wrap: wrap;
+
+      .status {
+        .success {
+          color: #f3920d !important;
+        }
+        .warning {
+          color: #1890ff !important;
+        }
+        .danger {
+          color: #f94b4b !important;
+        }
+      }
+
+      .col {
+        &:first-child {
+          width: 17vw;
+          color: #666;
+          // text-align: right;
+
+          &.max-width {
+            width: 100% !important;
+            margin-bottom: 2.6vw;
+          }
+        }
+
+        &:last-child {
+          color: #282828;
+        }
+      }
+
+      .el-image {
+        width: 25.6vw;
+        height: 25.6vw;
+      }
+    }
+
+    .control {
+      margin-top: 22.8vw;
+      .button {
+        width: 100%;
+        height: 12vw;
+
+        cursor: pointer;
+
+        &.edit {
+          @include themify($themes) {
+            border: 1px solid themed('color');
+            color: themed('color');
+          }
+        }
+
+        &.search {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 279 - 0
pages/_template/app/record/club/edit.vue

@@ -0,0 +1,279 @@
+<template>
+  <div class="club-info page">
+    <div class="page-top flex flex-col justify-center items-center">
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="name mt-2" v-text="supplierInfo.shopName + '认证记录'"></div>
+    </div>
+    <div class="page-content">
+      <div class="page-title">机构认证</div>
+      <FormClubInfo ref="formClubInfo" @step="onClubInfoFormStep" />
+      <div class="control flex flex-col items-center">
+        <div
+          class="button submit flex justify-center items-center"
+          @click="onSubmit"
+        >
+          提交
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import SimpleUploadImage from '@/components/SimpleUploadImage'
+import SimpleRadio from '@/components/SimpleRadio'
+import { mapGetters } from 'vuex'
+import FormClubInfo from '../../form/components/form-club-info.vue'
+export default {
+  layout: 'app',
+  components: {
+    SimpleUploadImage,
+    SimpleRadio,
+    FormClubInfo,
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix', 'clubUserId']),
+  },
+  data() {
+    return {
+      clubInfo: {},
+      formData: {},
+    }
+  },
+  created() {
+    this.fetchAuthDetail()
+  },
+  methods: {
+    async onSubmit() {
+      try {
+        await this.$refs.formClubInfo.validate()
+        this.formData.authUserId = this.authUserId
+        this.formData.source = 2
+        this.formData.authId = this.clubInfo.authId
+        await this.$http.api.authClubSave(this.formData)
+        this.$toast('保存成功')
+        this.$router.push(`${this.routePrefix}/record/club/detail`)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 获取认证机构信息
+    async fetchAuthDetail() {
+      try {
+        const result = await this.$http.api.fetchClubAuthInfo({
+          clubUserId: this.clubUserId,
+        })
+        this.formData.authId = result.data.auth.authId
+        const res = await this.$http.api.fetchClubAuthInfoData({
+          authId: result.data.auth.authId,
+        })
+        this.clubInfo = res.data
+        this.$refs.formClubInfo.init(this.clubInfo)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    onClubInfoFormStep(data) {
+      console.log(data)
+      this.formData = data
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+  .page-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('banner-club-register');
+      background-size: auto 360px;
+    }
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 700px;
+    margin: 0 auto;
+    overflow: hidden;
+    min-height: calc(100vh - 80px - 80px - 360px);
+    box-sizing: border-box;
+    padding-bottom: 40px;
+
+    .page-title {
+      font-size: 24px;
+      font-weight: bold;
+      text-align: center;
+      padding: 40px 0;
+    }
+
+    .control {
+      margin-top: 62px;
+      .button {
+        width: 295px;
+        height: 50px;
+
+        cursor: pointer;
+
+        &.submit {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+    }
+    .normal-row {
+      position: relative;
+      .label {
+        font-size: 14px;
+        color: #606266;
+
+        span {
+          color: #b2b2b2;
+        }
+      }
+      .postion-btn {
+        position: absolute;
+        top: 50%;
+        right: 0;
+        transform: translateY(-50%);
+        width: 62px;
+        height: 28px;
+        line-height: 28px;
+        font-size: 14px;
+        color: #fff;
+        background: #1890ff;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+        border-radius: 4px;
+
+        &::before {
+          content: '';
+          display: inline-block;
+          width: 16px;
+          height: 16px;
+          background: url(https://static.caimei365.com/www/authentic/pc/icon-position.png)
+            no-repeat center;
+          background-size: 16px 16px;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  ::v-deep {
+    .el-form-item__label {
+      font-size: 3.4vw;
+    }
+  }
+
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('banner-home-h5');
+      background-size: auto 46vw;
+    }
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+
+  .page-content {
+    box-sizing: border-box;
+    padding: 8vw 7vw;
+
+    .page-title {
+      font-size: 4.2vw;
+      font-weight: bold;
+      text-align: center;
+      margin-bottom: 8vw;
+      color: #282828;
+    }
+
+    .control {
+      .button {
+        width: 100%;
+        height: 12vw;
+
+        cursor: pointer;
+
+        &.submit {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+    }
+    .normal-row {
+      position: relative;
+      .label {
+        font-size: 14px;
+        color: #606266;
+
+        span {
+          color: #b2b2b2;
+          font-size: 2.6vw;
+        }
+      }
+      .postion-btn {
+        position: absolute;
+        top: 50%;
+        right: 0;
+        transform: translateY(-50%);
+        width: 14vw;
+        height: 6.8vw;
+        line-height: 6.8vw;
+        font-size: 3.2vw;
+        color: #fff;
+        background: #1890ff;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+        border-radius: 0.4vw;
+
+        &::before {
+          content: '';
+          display: inline-block;
+          width: 3.58vw;
+          height: 3.58vw;
+          background: url(https://static.caimei365.com/www/authentic/pc/icon-position.png)
+            no-repeat center;
+          background-size: 3.58vw;
+        }
+      }
+    }
+  }
+}
+</style>

+ 362 - 0
pages/_template/app/record/device/detail.vue

@@ -0,0 +1,362 @@
+<template>
+  <div class="page">
+    <div class="page-top flex flex-col justify-center items-center">
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="name mt-2" v-text="supplierInfo.shopName + '认证记录'"></div>
+    </div>
+    <div class="page-content">
+      <div class="page-title">设备认证</div>
+      <div class="row">
+        <div class="col">设备名称:</div>
+        <div class="col">{{ productInfo.productName }}</div>
+      </div>
+      <div class="row">
+        <div class="col">设备图片:</div>
+        <div class="col">
+          <el-image
+            v-if="productInfo.productImage"
+            :src="productInfo.productImage"
+            :preview-src-list="[productInfo.productImage]"
+          ></el-image>
+          <span v-else>暂无图片</span>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col">所属品牌:</div>
+        <div class="col">{{ productInfo.brandName }}</div>
+      </div>
+      <div class="row">
+        <div class="col">购买渠道:</div>
+        <div class="col">{{ productInfo.purchaseWay || '暂无' }}</div>
+      </div>
+      <div class="row">
+        <div class="col">发票:</div>
+        <div class="col">
+          <el-image
+            v-if="productInfo.invoiceImage"
+            :src="productInfo.invoiceImage"
+            :preview-src-list="[productInfo.invoiceImage]"
+          ></el-image>
+          <span v-else>暂无图片</span>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col">设备SN码:</div>
+        <div class="col">{{ productInfo.productName }}</div>
+      </div>
+      <div class="row">
+        <div class="col">设备参数:</div>
+        <div class="col">
+          <div class="params-list">
+            <div
+              class="param"
+              v-for="param in productInfo.paramList"
+              :key="param.productName"
+            >
+              <div class="param-name">{{ param.paramName }}:</div>
+              <div class="param-content">{{ param.paramContent }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col">状态:</div>
+        <div class="col" :class="auditStatusColor(productInfo.auditStatus)">
+          {{ productInfo.auditStatus | auditStatusFilter }}
+        </div>
+      </div>
+
+      <div class="row" v-if="productInfo.auditStatus === 0">
+        <div class="col">原因:</div>
+        <div class="col">
+          {{ productInfo.invalidReason ? productInfo.invalidReason : '暂无' }}
+        </div>
+      </div>
+
+      <div
+        class="control flex flex-col items-center"
+        v-if="productInfo.auditStatus === 0"
+      >
+        <div
+          class="button edit flex justify-center items-center"
+          @click="onEdit"
+        >
+          编辑
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app',
+  data() {
+    return {
+      productId: '',
+      productInfo: {},
+    }
+  },
+  filters: {
+    auditStatusFilter(value) {
+      // 认证状态:0审核未通过,1审核通过,2待审核
+      const map = {
+        0: '审核未通过',
+        1: '审核通过',
+        2: '待审核',
+      }
+      return map[value]
+    },
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix']),
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      this.productId = this.$route.query.id
+      this.getProductDetails()
+    },
+    // 获取认证机构信息
+    async getProductDetails() {
+      try {
+        const res = await this.$http.api.getProductDetails({
+          productId: this.productId,
+        })
+        this.productInfo = { ...this.productInfo, ...res.data }
+        console.log('res', this.productInfo)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    auditStatusColor(value) {
+      // 认证状态:0 danger,1 success,2 warning
+      const map = {
+        0: 'danger',
+        1: 'success',
+        2: 'warning',
+      }
+      return map[value]
+    },
+    onEdit() {
+      this.$router.push(
+        `${this.routePrefix}/record/device/edit?type=edit&id=${this.productId}`
+      )
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('banner-club-register');
+      background-size: auto 360px;
+    }
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 600px;
+    margin: 0 auto;
+    overflow: hidden;
+    min-height: calc(100vh - 80px - 80px - 360px);
+    box-sizing: border-box;
+    padding-bottom: 40px;
+
+    .page-title {
+      font-size: 24px;
+      font-weight: bold;
+      text-align: center;
+      padding: 40px 0;
+    }
+
+    .params-list {
+      .param {
+        display: grid;
+        grid-template-columns: repeat(2, 1fr);
+        grid-column-gap: 8px;
+        grid-row-gap: 16px;
+        .param-name {
+          text-align: right;
+        }
+      }
+    }
+
+    .row {
+      display: flex;
+      justify-content: flex-start;
+      align-items: flex-start;
+      font-size: 18px;
+      margin: 24px 0;
+
+      .col {
+        &.success {
+          color: #f3920d !important;
+        }
+        &.warning {
+          color: #1890ff !important;
+        }
+        &.danger {
+          color: #f94b4b !important;
+        }
+        &:first-child {
+          width: 100px;
+          color: #666;
+          text-align: right;
+        }
+
+        &:last-child {
+          color: #282828;
+        }
+      }
+
+      .el-image {
+        width: 120px;
+        height: 120px;
+        margin-right: 12px;
+        box-sizing: border-box;
+        border-radius: 4px;
+      }
+    }
+
+    .control {
+      margin-top: 62px;
+      .button {
+        width: 295px;
+        height: 50px;
+
+        cursor: pointer;
+
+        &.edit {
+          @include themify($themes) {
+            border: 1px solid themed('color');
+            color: themed('color');
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('banner-home-h5');
+      background-size: auto 46vw;
+    }
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+
+  .page-content {
+    box-sizing: border-box;
+    padding: 8vw 7vw;
+
+    .page-title {
+      font-size: 4.2vw;
+      font-weight: bold;
+      text-align: center;
+      color: #282828;
+      margin-bottom: 4.6vw;
+    }
+
+    .params-list {
+      .param {
+        display: grid;
+        grid-template-columns: repeat(2, 1fr);
+        grid-column-gap: 1.2vw;
+        grid-row-gap: 2.4vw;
+        .param-name {
+          text-align: right;
+        }
+      }
+    }
+
+    .row {
+      display: flex;
+      justify-content: flex-start;
+      align-items: flex-start;
+      font-size: 3.4vw;
+      margin: 5.6vw 0;
+
+      .col {
+        &.success {
+          color: #f3920d !important;
+        }
+        &.warning {
+          color: #1890ff !important;
+        }
+        &.danger {
+          color: #f94b4b !important;
+        }
+        &:first-child {
+          width: 19vw;
+          color: #666;
+          white-space: nowrap;
+          flex-shrink: 0;
+          // text-align: right;
+        }
+
+        &:last-child {
+          color: #282828;
+        }
+      }
+
+      .el-image {
+        width: 26vw;
+        height: 26vw;
+        border-radius: 1vw;
+      }
+    }
+
+    .control {
+      margin-top: 22.8vw;
+      .button {
+        width: 100%;
+        height: 12vw;
+        cursor: pointer;
+
+        &.edit {
+          @include themify($themes) {
+            border: 1px solid themed('color');
+            color: themed('color');
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 253 - 0
pages/_template/app/record/device/edit.vue

@@ -0,0 +1,253 @@
+<template>
+  <div class="club-device page">
+    <div class="page-top flex flex-col justify-center items-center">
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="name mt-2" v-text="supplierInfo.shopName + '认证记录'"></div>
+    </div>
+    <div class="page-content">
+      <div class="page-title">设备认证</div>
+      <FormClubDevice ref="formClubDevice" :formType="formType" @step="onClubDeviceFormStep" />
+      <div class="control flex flex-col items-center">
+        <div class="button submit flex justify-center items-center" @click="onSubmit">提交</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import FormClubDevice from '../../form/components/form-club-device.vue'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app',
+  components: {
+    FormClubDevice
+  },
+  data() {
+    return {
+      productInfo: {},
+      formData: {},
+      productId:0,
+      formType:'',
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix','authId']),
+  },
+  mounted() {
+    this.formType = this.$route.query.type
+    this.getProductDetails()
+  },
+  methods: {
+    async onSubmit() {
+      try {
+        await this.$refs.formClubDevice.validate()
+        if(this.formType === 'edit'){
+          this.formData.authId = this.authId
+        }
+        await this.$http.api.authProducSave(this.formData)
+        this.$toast('保存成功')
+        this.$router.push(`${this.routePrefix}/record/device/detail?id=${this.productId}`)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 获取认证机构信息
+    async getProductDetails() {
+      try {
+        this.productId = this.$route.query.id
+        const res = await this.$http.api.getProductDetails({
+          productId: this.productId,
+        })
+        this.productInfo = res.data
+        console.log('productInfo',this.productInfo)
+        this.$refs.formClubDevice.init(this.productInfo)
+      } catch (error) {}
+    },
+    onClubDeviceFormStep(data) {
+      console.log(data)
+       this.formData = data[0]
+    }
+  }  
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+  .page-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('banner-club-register');
+      background-size: auto 360px;
+    }
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 700px;
+    margin: 0 auto;
+    overflow: hidden;
+    min-height: calc(100vh - 80px - 80px - 360px);
+    box-sizing: border-box;
+    padding-bottom: 40px;
+
+    .page-title {
+      font-size: 24px;
+      font-weight: bold;
+      text-align: center;
+      padding: 40px 0;
+    }
+
+    .el-select {
+      width: 100%;
+    }
+    .control {
+      margin-top: 62px;
+      .button {
+        width: 295px;
+        height: 50px;
+
+        cursor: pointer;
+
+        &.submit {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+    }
+
+    .device-param-list {
+      position: relative;
+      .add-param {
+        position: absolute;
+        cursor: pointer;
+        top: -40px;
+        right: 0;
+        text-decoration: underline;
+        font-size: 14px;
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+
+      .param {
+        position: relative;
+        .remove {
+          position: absolute;
+          right: 0;
+          top: 0;
+          width: 20px;
+          height: 20px;
+          background: #f94b4b;
+          border-radius: 2px;
+          cursor: pointer;
+          color: #fff;
+          font-size: 14px;
+          text-align: center;
+          line-height: 20px;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('banner-home-h5');
+      background-size: auto 46vw;
+    }
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+  .page-content {
+    box-sizing: border-box;
+    padding: 8vw 7vw;
+
+    .page-title {
+      font-size: 4.2vw;
+      font-weight: bold;
+      text-align: center;
+      color: #282828;
+      margin-bottom: 4.6vw;
+    }
+
+    .el-select {
+      width: 100%;
+    }
+    .control {
+      .button {
+        width: 100%;
+        height: 12vw;
+
+        cursor: pointer;
+
+        &.submit {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+    }
+
+    .device-param-list {
+      position: relative;
+      .add-param {
+        position: absolute;
+        cursor: pointer;
+        top: -40px;
+        right: 0;
+        font-size: 3.4vw;
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+
+      .param {
+        position: relative;
+        .remove {
+          position: absolute;
+          right: 0;
+          top: 0;
+          width: 4.4vw;
+          height: 4.4vw;
+          background: #f94b4b;
+          border-radius: 0.2vw;
+          cursor: pointer;
+          color: #fff;
+          font-size: 3.4vw;
+          text-align: center;
+          line-height: 4.4vw;
+        }
+      }
+    }
+  }
+}
+</style>

+ 321 - 0
pages/_template/app/record/device/index.vue

@@ -0,0 +1,321 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <div
+          class="name mt-2"
+          v-text="supplierInfo.shopName + '认证记录'"
+        ></div>
+      </div>
+      <div class="page-content">
+        <template v-if="list.length > 0">
+          <div class="page-title">设备认证</div>
+          <div
+            class="device-list"
+            v-for="item in list"
+            :key="item.productId"
+            @click="toEdit(item)"
+          >
+            <div class="device">
+              <div class="name">
+                <span class="label">设备名称:</span>
+                <span class="content">{{
+                  item.productName ? item.productName : ''
+                }}</span>
+              </div>
+              <div class="status" :class="auditStatusColor(item.auditStatus)">
+                <span class="label">状态:</span>
+                <span class="content">{{
+                  item.auditStatus | auditStatusFilter
+                }}</span>
+              </div>
+            </div>
+          </div>
+        </template>
+        <template v-else>
+          <SimpleEmpty name="icon-device-empty.png" description="暂无设备~" />
+        </template>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import SimpleEmpty from '@/components/SimpleEmpty'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app',
+  components: {
+    SimpleEmpty,
+  },
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      list: [],
+      listQuery: {
+        authId: 0,
+        listType: 2,
+        pageNum: 1,
+        pageSize: 10,
+      },
+      total: 0,
+    }
+  },
+  filters: {
+    auditStatusFilter(value) {
+      // 认证状态:0审核未通过,1审核通过,2待审核
+      const map = {
+        0: '审核未通过',
+        1: '审核通过',
+        2: '待审核',
+      }
+      return map[value]
+    },
+  },
+  created() {
+    this.initData()
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix', 'authId']),
+  },
+  methods: {
+    toEdit(item) {
+      this.$router.push(
+        `${this.routePrefix}/record/device/detail?id=${item.productId}`
+      )
+    },
+    initData() {
+      this.listQuery.authId = this.$route.query.authId
+      if(!this.listQuery.authId) return
+      this.authProductList()
+    },
+    // 获取机构列表
+    async authProductList() {
+      try {
+        this.isLoadingMore = true
+        const res = await this.$http.api.getClubAuthProductList(this.listQuery)
+        this.total = res.data.total
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.isLoadingMore = false
+        this.listQuery.pageNum += 1
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.$toast.clear()
+        this.isRequest = false
+      }
+    },
+    auditStatusColor(value) {
+      // 认证状态:0 danger,1 success,2 warning
+      const map = {
+        0: 'danger',
+        1: 'success',
+        2: 'warning',
+      }
+      return map[value]
+    },
+    // 加载更多
+    onLoadMore() {
+      this.authProductList()
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('banner-club-register');
+      background-size: auto 360px;
+    }
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 700px;
+    margin: 0 auto;
+    overflow: hidden;
+    min-height: calc(100vh - 80px - 80px - 360px);
+    box-sizing: border-box;
+    padding-bottom: 40px;
+
+    .page-title {
+      font-size: 24px;
+      font-weight: bold;
+      text-align: center;
+      padding: 40px 0;
+    }
+
+    .device-list {
+      .device {
+        position: relative;
+        padding: 36px 0 12px;
+        border-bottom: 1px solid #c2c2c2;
+        cursor: pointer;
+
+        .name {
+          margin-bottom: 8px;
+        }
+
+        .label {
+          font-size: 18px;
+          color: #666;
+        }
+
+        .content {
+          font-size: 18px;
+          color: #282828;
+        }
+        .status {
+          &.success {
+            .content {
+              color: #f3920d;
+            }
+          }
+          &.warning {
+            .content {
+              color: #1890ff;
+            }
+          }
+          &.danger {
+            .content {
+              color: #f94b4b;
+            }
+          }
+        }
+
+        &::after {
+          content: '';
+          position: absolute;
+          right: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          display: block;
+          width: 20px;
+          height: 20px;
+          background: url(https://static.caimei365.com/www/authentic/pc/icon-detail-more.png)
+            no-repeat center;
+          background-size: 18px;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('banner-home-h5');
+      background-size: auto 46vw;
+    }
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+
+  .page-content {
+    box-sizing: border-box;
+    padding: 8vw 7vw;
+
+    .page-title {
+      font-size: 4.2vw;
+      font-weight: bold;
+      text-align: center;
+      color: #282828;
+      margin-bottom: 4.6vw;
+    }
+
+    .device-list {
+      .device {
+        position: relative;
+        padding: 2.6vw 0;
+        border-bottom: 0.1vw solid #c2c2c2;
+        cursor: pointer;
+
+        .name {
+          margin-bottom: 2.2vw;
+        }
+
+        .label {
+          font-size: 3.4vw;
+          color: #666;
+        }
+
+        .content {
+          font-size: 3.4vw;
+          color: #282828;
+        }
+        .status {
+          &.success {
+            .content {
+              color: #f3920d;
+            }
+          }
+          &.warning {
+            .content {
+              color: #1890ff;
+            }
+          }
+          &.danger {
+            .content {
+              color: #f94b4b;
+            }
+          }
+        }
+
+        &::after {
+          content: '';
+          position: absolute;
+          right: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          display: block;
+          width: 20px;
+          height: 20px;
+          background: url(https://static.caimei365.com/www/authentic/pc/icon-detail-more.png)
+            no-repeat center;
+          background-size: 18px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 116 - 0
pages/_template/app/record/message.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="flex justify-center page">
+    <div class="page-content flex flex-col items-center">
+      <div class="icon-submit-succsss"></div>
+      <div class="tip mt-4 mb-6">提交成功</div>
+      <div class="label text-center">
+        审核通过后,用户可通过正品授权入口或扫二维码查看 机构及设备正品授权信息
+      </div>
+      <div class="record-btn mt-4" @click="toClubRecord">认证记录</div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app',
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix', 'accessToken']),
+  },
+  methods: {
+    toClubRecord() {
+      if (this.accessToken) {
+        this.$router.push(`${this.routePrefix}/record/club/detail`)
+      } else {
+        this.$toast('请登录后查看')
+        this.$router.push(this.routePrefix)
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    min-height: calc(100vh - 80px - 80px);
+    background: #fff;
+  }
+  .page-content {
+    width: 432px;
+    padding-top: 140px;
+
+    .icon-submit-succsss {
+      width: 64px;
+      height: 64px;
+      background: url(https://static.caimei365.com/www/authentic/pc/icon-submit-success.png)
+        no-repeat center;
+      background-size: 64px;
+    }
+
+    .tip {
+      font-size: 24px;
+      font-weight: bold;
+      color: #1890ff;
+    }
+    .label {
+      font-size: 18px;
+      color: #282828;
+      line-height: 1.6;
+    }
+    .record-btn {
+      width: 98px;
+      height: 36px;
+      color: #fff;
+      font-size: 14px;
+      text-align: center;
+      line-height: 36px;
+      border-radius: 4px;
+      cursor: pointer;
+      @include themify($themes) {
+        background: themed('color');
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page-content {
+    padding: 0 9.2vw;
+    padding-top: 25.2vw;
+
+    .icon-submit-succsss {
+      width: 12vw;
+      height: 12vw;
+      background: url(https://static.caimei365.com/www/authentic/pc/icon-submit-success.png)
+        no-repeat center;
+      background-size: 12vw;
+    }
+
+    .tip {
+      font-size: 4.2vw;
+      font-weight: bold;
+      color: #1890ff;
+    }
+    .label {
+      font-size: 3.4vw;
+      color: #282828;
+      line-height: 1.6;
+    }
+    .record-btn {
+      width: 36vw;
+      height: 8.8vw;
+      color: #fff;
+      font-size: 3.2vw;
+      text-align: center;
+      line-height: 8.8vw;
+      border-radius: 0.4vw;
+      cursor: pointer;
+      @include themify($themes) {
+        background: themed('color');
+      }
+    }
+  }
+}
+</style>

+ 138 - 0
pages/_template/app/record/search.vue

@@ -0,0 +1,138 @@
+<template>
+  <div class="page">
+    <div class="page-top flex flex-col justify-center items-center">
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="name mt-2" v-text="supplierInfo.shopName + '认证记录'"></div>
+    </div>
+    <div class="page-content">
+      <el-form>
+        <el-form-item label="手机号:">
+          <el-input placeholder="请输入注册账号手机"></el-input>
+        </el-form-item>
+      </el-form>
+      <div class="control flex flex-col items-center">
+        <div
+          class="button search flex justify-center items-center"
+          @click="onSearch"
+        >
+          查询
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app',
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix']),
+  },
+  methods: {
+    onSearch() {
+      this.$router.push(`${this.routePrefix}/record/club/detail`)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('banner-club-register');
+      background-size: auto 360px;
+    }
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 700px;
+    margin: 0 auto;
+    overflow: hidden;
+    min-height: calc(100vh - 80px - 80px - 360px);
+    box-sizing: border-box;
+    padding-bottom: 40px;
+    padding-top: 70px;
+
+    .control {
+      margin-top: 62px;
+      .button {
+        width: 295px;
+        height: 50px;
+
+        cursor: pointer;
+
+        &.search {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('banner-home-h5');
+      background-size: auto 46vw;
+    }
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+
+  .page-content {
+    padding: 0 7.2vw;
+    padding-top: 18vw;
+    box-sizing: border-box;
+
+    .control {
+      margin-top: 12.8vw;
+      .button {
+        width: 100%;
+        height: 12vw;
+        cursor: pointer;
+
+        &.search {
+          @include themify($themes) {
+            background-color: themed('color');
+            color: #fff;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 72 - 0
pages/_template/app/user/forget.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="logo-form">
+    <div class="form-item">
+      <input type="text" placeholder="手机号" />
+    </div>
+    <div class="form-item">
+      <input type="text" placeholder="验证码" />
+    </div>
+    <div class="form-item">
+      <input type="text" placeholder="输入8-12位密码" />
+      <span class="send">发送验证码</span>
+    </div>
+    <div class="form-control">
+      <div class="submit">登录</div>
+      <div class="control flex justify-between items-center mt-3">
+        <span class="forget">忘记密码?</span>
+        <span class="register">立即注册</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {}
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.logo-form {
+  padding: 7.2vw;
+  .form-item {
+    position: relative;
+    padding: 2.5vw 0;
+    border-bottom: 1px solid #d8d8d8;
+    margin-bottom: 3.2vw;
+
+    .send {
+      position: absolute;
+      right: 0;
+      bottom: 2.5vw;
+      font-size: 3.6vw;
+      color: #f3920d;
+    }
+  }
+
+  .form-control {
+    margin-top: 20.8vw;
+    .submit {
+      width: 100%;
+      height: 12vw;
+      text-align: center;
+      line-height: 12vw;
+      background: #f3920d;
+      color: #fff;
+    }
+
+    .control {
+      font-size: 3.6vw;
+
+      .forget {
+        color: #f3920d;
+      }
+      .register {
+        color: #f3920d;
+      }
+    }
+  }
+}
+</style>

+ 88 - 0
pages/_template/app/user/login.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="logo-form">
+    <div class="form-item">
+      <input type="text" placeholder="手机号" />
+    </div>
+    <div class="form-item">
+      <input type="text" placeholder="验证码" />
+    </div>
+    <div class="form-control">
+      <div class="submit" @click="active = true">登录</div>
+      <div class="control flex justify-between items-center mt-3">
+        <span class="forget">忘记密码?</span>
+        <span class="register">立即注册</span>
+      </div>
+    </div>
+    <SimpleDialog v-model="active" @click="onClick" />
+  </div>
+</template>
+
+<script>
+import SimpleDialog from '@/components/SimpleDialog'
+export default {
+  components: {
+    SimpleDialog,
+  },
+  data() {
+    return {
+      active: false,
+    }
+  },
+
+  methods: {
+    onClick() {
+      this.active = false
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.logo-form {
+  padding: 7.2vw;
+  .form-item {
+    position: relative;
+    padding: 2.5vw 0;
+    border-bottom: 1px solid #d8d8d8;
+    margin-bottom: 3.2vw;
+
+    input {
+      &::placeholder {
+        color: #b2b2b2;
+        font-size: 3.4vw;
+      }
+    }
+
+    .send {
+      position: absolute;
+      right: 0;
+      bottom: 2.5vw;
+      font-size: 3.6vw;
+      color: #f3920d;
+    }
+  }
+
+  .form-control {
+    margin-top: 20.8vw;
+    .submit {
+      width: 100%;
+      height: 12vw;
+      text-align: center;
+      line-height: 12vw;
+      background: #f3920d;
+      color: #fff;
+    }
+
+    .control {
+      font-size: 3.6vw;
+
+      .forget {
+        color: #f3920d;
+      }
+      .register {
+        color: #f3920d;
+      }
+    }
+  }
+}
+</style>

+ 72 - 0
pages/_template/app/user/register.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="logo-form">
+    <div class="form-item">
+      <input type="text" placeholder="手机号" />
+    </div>
+    <div class="form-item">
+      <input type="text" placeholder="验证码" />
+    </div>
+    <div class="form-item">
+      <input type="text" placeholder="输入8-12位密码" />
+      <span class="send">发送验证码</span>
+    </div>
+    <div class="form-control">
+      <div class="submit">登录</div>
+      <div class="control flex justify-between items-center mt-3">
+        <span class="forget">忘记密码?</span>
+        <span class="register">立即注册</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {}
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.logo-form {
+  padding: 7.2vw;
+  .form-item {
+    position: relative;
+    padding: 2.5vw 0;
+    border-bottom: 1px solid #d8d8d8;
+    margin-bottom: 3.2vw;
+
+    .send {
+      position: absolute;
+      right: 0;
+      bottom: 2.5vw;
+      font-size: 3.6vw;
+      color: #f3920d;
+    }
+  }
+
+  .form-control {
+    margin-top: 20.8vw;
+    .submit {
+      width: 100%;
+      height: 12vw;
+      text-align: center;
+      line-height: 12vw;
+      background: #f3920d;
+      color: #fff;
+    }
+
+    .control {
+      font-size: 3.6vw;
+
+      .forget {
+        color: #f3920d;
+      }
+      .register {
+        color: #f3920d;
+      }
+    }
+  }
+}
+</style>

+ 6 - 2
pages/auth/index.vue

@@ -175,12 +175,16 @@ export default {
         top: 50%;
         transform: translateY(-50%);
         font-size: 3.4vw;
-        color: #bc1724;
+        @include themify($themes) {
+          color: themed('color');
+        }
       }
     }
 
     .submit {
-      background: #bc1724;
+      @include themify($themes) {
+        background: themed('color');
+      }
       font-size: 3.2vw;
       color: #fff;
       text-align: center;

+ 4 - 0
plugins/element-ui.js

@@ -0,0 +1,4 @@
+import Vue from 'vue'
+import ElementUI from 'element-ui'
+
+Vue.use(ElementUI)

BIN
static/location.png


+ 5 - 0
static/map.config.js

@@ -0,0 +1,5 @@
+window._AMapSecurityConfig = {
+  // serviceHost: '您的代理服务器域名或地址/_AMapService',
+  securityJsCode: 'ff3114e0be935539f0ca5c9a80406752',
+  // 例如 :serviceHost:'http://1.1.1.1:80/_AMapService',
+}

+ 5 - 0
store/app.js

@@ -4,6 +4,7 @@ const state = () => ({
   static: `${process.env.STATIC_URL}/pc`,
   loginVisiable: false,
   routePrefix: '', // 路由前缀
+  themeName: 'normal',
 })
 
 const mutations = {
@@ -27,6 +28,10 @@ const mutations = {
   SET_ROUTE_PREFIX(state, prefix) {
     state.routePrefix = prefix
   },
+  // 设置主题
+  SET_PAGE_THEME(state, name) {
+    state.themeName = name
+  },
 }
 
 const actions = {}

+ 3 - 0
store/getters.js

@@ -5,12 +5,15 @@ export default {
   static: (state) => state.app.static,
   routePrefix: (state) => state.app.routePrefix,
   loginVisiable: (state) => state.app.loginVisiable,
+  themeName: (state) => state.app.themeName,
   // 用户相关
   userInfo: (state) => state.user.userInfo,
   authUserId: (state) => state.user.authUserId,
+  authId: (state) => state.user.authId,
   accessToken: (state) => state.user.accessToken,
   appId: (state) => state.user.appId,
   accountType: (state) => state.user.accountType,
+  clubUserId: (state) => state.user.clubUserId,
   // 供应商相关
   supplierInfo: (state) => state.supplier.supplierInfo,
 }

+ 4 - 0
store/user.js

@@ -4,12 +4,16 @@ const state = () => ({
   accessToken: '',
   appId: '',
   accountType: '',
+  clubUserId: '',
+  authId: '',
 })
 
 const mutations = {
   // 设置用户信息
   SET_USER_INFO(state, data) {
     state.userInfo = data
+    state.clubUserId = data.clubUserId
+    state.authId = data.authId
     state.accessToken = data.accessToken
   },
   // 设置appId

+ 28 - 0
themes/themeMixin.scss

@@ -0,0 +1,28 @@
+@import './themeVariable.scss';
+
+$theme-map: null;
+@mixin themify($themes: $themes) {
+  @each $theme-name, $map in $themes {
+    // & 表示父级元素
+    // !global 表示覆盖原来的
+    .theme-#{$theme-name} & {
+      $theme-map: () !global;
+      // 循环合并键值对
+      @each $key, $value in $map {
+        $theme-map: map-merge(
+          $theme-map,
+          (
+            $key: $value,
+          )
+        ) !global;
+      }
+      // 表示包含 下面函数 themed()
+      @content;
+      $theme-map: null !global;
+    }
+  }
+}
+
+@function themed($key) {
+  @return map-get($theme-map, $key);
+}

+ 7 - 0
themes/themeVariable.scss

@@ -0,0 +1,7 @@
+@import './variables/normal.scss';
+@import './variables/ross.scss';
+
+$themes: (
+  normal: $mormalTuple,
+  ross: $rossTuple,
+);

+ 60 - 0
themes/variables/normal.scss

@@ -0,0 +1,60 @@
+$mormalTuple: (
+  color: #bc1724,
+  sub-color: #ffe6e8,
+  hover-color: #960915,
+  cover-color: linear-gradient(180deg, #ffe6e8 0%, rgba(255, 255, 255, 0) 100%),
+  // pc端
+  icon-doctor-level-pc:
+    url('https://static.caimei365.com/www/authentic/pc/icon-doctor-level.png'),
+  icon-phone-active-pc:
+    url('https://static.caimei365.com/www/authentic/pc/icon-phone-active.png'),
+  icon-address-active-pc:
+    url('https://static.caimei365.com/www/authentic/pc/icon-address-active.png'),
+  icon-navigation-pc:
+    url('https://static.caimei365.com/www/authentic/pc/icon-navigation.png'),
+  banner-home-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-home.png'),
+  banner-approve-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-approve.png'),
+  banner-club-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-club.png'),
+  banner-device-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-device.png'),
+  banner-personnel-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-approve.png'),
+  banner-personnel-operate-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-doctor.png'),
+  banner-database-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-doc.png'),
+  banner-feedback-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-doc.png'),
+  banner-club-register:
+    url('https://static.caimei365.com/www/authentic/pc/ross-bg-club-register.png'),
+  // 移动端
+  icon-doctor-level-h5:
+    url('https://static.caimei365.com/www/authentic/h5/icon-doctor-level.png'),
+  icon-navigation-h5:
+    url('https://static.caimei365.com/www/authentic/h5/icon-navigation.png'),
+  icon-phone-active-h5:
+    url('https://static.caimei365.com/www/authentic/h5/icon-phone-active.png'),
+  icon-address-active-h5:
+    url('https://static.caimei365.com/www/authentic/h5/icon-address-active.png'),
+  icon-arround-right-h5:
+    url('https://static.caimei365.com/www/authentic/h5/icon-arround-right.png'),
+  banner-home-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-home.png'),
+  banner-approve-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-approve.png'),
+  banner-club-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-club.png'),
+  banner-device-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-device.png'),
+  banner-personnel-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-approve.png'),
+  banner-personnel-operate-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-doctor.png'),
+  banner-database-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-doc.png'),
+  banner-feedback-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-doc.png'),
+);

+ 62 - 0
themes/variables/ross.scss

@@ -0,0 +1,62 @@
+$rossTuple: (
+  color: #f3920d,
+  hover-color: #d68616,
+  sub-color: #ffebcf,
+  cover-color: linear-gradient(180deg, #ffebcf 0%, rgba(255, 255, 255, 0) 100%),
+  // pc端
+  icon-doctor-level-pc:
+    url('https://static.caimei365.com/www/authentic/pc/icon-doctor-level.png'),
+  icon-navigation-pc:
+    url('https://static.caimei365.com/www/authentic/pc/ross-icon-navigation.png'),
+  icon-phone-active-pc:
+    url('https://static.caimei365.com/www/authentic/pc/ross-icon-phone-active.png'),
+  icon-address-active-pc:
+    url('https://static.caimei365.com/www/authentic/pc/ross-icon-address-active.png'),
+  icon-arround-right-pc:
+    url('https://static.caimei365.com/www/authentic/pc/ross-icon-arround-right.png'),
+  banner-home-pc:
+    url('https://static.caimei365.com/www/authentic/pc/ross-bg-home.png'),
+  banner-approve-pc:
+    url('https://static.caimei365.com/www/authentic/pc/ross-bg-approve.png'),
+  banner-club-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-club.png'),
+  banner-device-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-device.png'),
+  banner-personnel-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-approve.png'),
+  banner-personnel-operate-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-doctor.png'),
+  banner-database-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-doc.png'),
+  banner-feedback-pc:
+    url('https://static.caimei365.com/www/authentic/pc/bg-doc.png'),
+  banner-club-register:
+    url('https://static.caimei365.com/www/authentic/pc/ross-bg-club-register.png'),
+  // 移动端
+  icon-doctor-level-h5:
+    url('https://static.caimei365.com/www/authentic/h5/icon-doctor-level.png'),
+  icon-navigation-h5:
+    url('https://static.caimei365.com/www/authentic/h5/ross-icon-navigation.png'),
+  icon-phone-active-h5:
+    url('https://static.caimei365.com/www/authentic/h5/ross-icon-phone-active.png'),
+  icon-address-active-h5:
+    url('https://static.caimei365.com/www/authentic/h5/ross-icon-address-active.png'),
+  icon-arround-right-h5:
+    url('https://static.caimei365.com/www/authentic/h5/ross-icon-arround-right.png'),
+  banner-home-h5:
+    url('https://static.caimei365.com/www/authentic/h5/ross-bg-home.png'),
+  banner-approve-h5:
+    url('https://static.caimei365.com/www/authentic/h5/ross-bg-approve.png'),
+  banner-club-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-club.png'),
+  banner-device-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-device.png'),
+  banner-personnel-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-approve.png'),
+  banner-personnel-operate-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-doctor.png'),
+  banner-database-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-doc.png'),
+  banner-feedback-h5:
+    url('https://static.caimei365.com/www/authentic/h5/bg-doc.png'),
+);

+ 18 - 0
utils/validator.js

@@ -13,3 +13,21 @@ export function isWeChat() {
     ua.match(/MicroMessenger/i).includes('micromessenger')
   )
 }
+
+/**
+ * @param {String} arg
+ * @returns {Boolean}
+ */
+export function isPoint(arg) {
+  const reg = /^[0-9|\.]{1,},[0-9|\.]{1,}$/
+  return reg.test(arg)
+}
+
+/**
+ * @param {String} arg
+ * @returns {Boolean}
+ */
+export function isNumber(arg) {
+  const reg = /\d+$/
+  return reg.test(arg)
+}

Some files were not shown because too many files changed in this diff