Browse Source

禾雅堂模板上测试

yuwenjun1997 2 years ago
parent
commit
2960895085
98 changed files with 11535 additions and 8 deletions
  1. BIN
      assets/theme-images/hyt/h5/banner-club.png
  2. BIN
      assets/theme-images/hyt/h5/banner-device.png
  3. BIN
      assets/theme-images/hyt/h5/banner-doc.png
  4. BIN
      assets/theme-images/hyt/h5/banner-doctor.png
  5. BIN
      assets/theme-images/hyt/h5/banner-feedback.png
  6. BIN
      assets/theme-images/hyt/h5/banner-record.png
  7. BIN
      assets/theme-images/hyt/h5/banner-register.png
  8. BIN
      assets/theme-images/hyt/h5/club-info-bg.png
  9. BIN
      assets/theme-images/hyt/h5/h5-icon-mobile.png
  10. BIN
      assets/theme-images/hyt/h5/icon-address.png
  11. BIN
      assets/theme-images/hyt/h5/icon-auth-ren.png
  12. BIN
      assets/theme-images/hyt/h5/icon-auth-seal.png
  13. BIN
      assets/theme-images/hyt/h5/icon-auth.png
  14. BIN
      assets/theme-images/hyt/h5/icon-auth2.png
  15. BIN
      assets/theme-images/hyt/h5/icon-avatar-v.png
  16. BIN
      assets/theme-images/hyt/h5/icon-club-logo-default.png
  17. BIN
      assets/theme-images/hyt/h5/icon-device-list.png
  18. BIN
      assets/theme-images/hyt/h5/icon-feedback-submit.png
  19. BIN
      assets/theme-images/hyt/h5/icon-navigation.png
  20. BIN
      assets/theme-images/hyt/h5/link-entry-doc-active.png
  21. BIN
      assets/theme-images/hyt/h5/link-entry-feedback-active.png
  22. BIN
      assets/theme-images/hyt/h5/link-entry-register-active.png
  23. BIN
      assets/theme-images/hyt/h5/nav-entry-device-active.png
  24. BIN
      assets/theme-images/hyt/h5/nav-entry-doctor-active.png
  25. BIN
      assets/theme-images/hyt/logo.png
  26. BIN
      assets/theme-images/hyt/pc/banner-club.jpg
  27. BIN
      assets/theme-images/hyt/pc/banner-device.png
  28. BIN
      assets/theme-images/hyt/pc/banner-doc.png
  29. BIN
      assets/theme-images/hyt/pc/banner-doctor.png
  30. BIN
      assets/theme-images/hyt/pc/banner-feedback.png
  31. BIN
      assets/theme-images/hyt/pc/banner-record.png
  32. BIN
      assets/theme-images/hyt/pc/banner-register.png
  33. BIN
      assets/theme-images/hyt/pc/club-info-bg.png
  34. BIN
      assets/theme-images/hyt/pc/icon-address.png
  35. BIN
      assets/theme-images/hyt/pc/icon-auth-ren.png
  36. BIN
      assets/theme-images/hyt/pc/icon-auth-seal.png
  37. BIN
      assets/theme-images/hyt/pc/icon-auth.png
  38. BIN
      assets/theme-images/hyt/pc/icon-auth2.png
  39. BIN
      assets/theme-images/hyt/pc/icon-avatar-v.png
  40. BIN
      assets/theme-images/hyt/pc/icon-center-item-auth-club.png
  41. BIN
      assets/theme-images/hyt/pc/icon-center-item-device.png
  42. BIN
      assets/theme-images/hyt/pc/icon-club-logo-default.png
  43. BIN
      assets/theme-images/hyt/pc/icon-device-list.png
  44. BIN
      assets/theme-images/hyt/pc/icon-download-hover.png
  45. BIN
      assets/theme-images/hyt/pc/icon-feedback-submit.png
  46. BIN
      assets/theme-images/hyt/pc/icon-mobile.png
  47. BIN
      assets/theme-images/hyt/pc/icon-navigation.png
  48. BIN
      assets/theme-images/hyt/pc/link-entry-doc-active.png
  49. BIN
      assets/theme-images/hyt/pc/link-entry-doc.png
  50. BIN
      assets/theme-images/hyt/pc/link-entry-feedback-active.png
  51. BIN
      assets/theme-images/hyt/pc/link-entry-feedback.png
  52. BIN
      assets/theme-images/hyt/pc/link-entry-register-active.png
  53. BIN
      assets/theme-images/hyt/pc/link-entry-register.png
  54. BIN
      assets/theme-images/hyt/pc/logo.png
  55. BIN
      assets/theme-images/hyt/pc/nav-entry-device-active.png
  56. BIN
      assets/theme-images/hyt/pc/nav-entry-device.png
  57. BIN
      assets/theme-images/hyt/pc/nav-entry-doctor-active.png
  58. BIN
      assets/theme-images/hyt/pc/nav-entry-doctor.png
  59. 2 0
      assets/themes/themeVariable.scss
  60. 6 0
      assets/themes/variables/hyt.scss
  61. 1 1
      assets/themes/variables/normal.scss
  62. 3 2
      components/SimpleCenterCover/index.vue
  63. 28 1
      components/SimpleLogin/index.vue
  64. 4 0
      configs/mode-map.js
  65. 634 0
      layouts/app-hyt.vue
  66. 1 1
      middleware/intercept.js
  67. 1 1
      pages/_template/app/feedback/index.vue
  68. 672 0
      pages/_template/hyt/approve/club/detail.vue
  69. 432 0
      pages/_template/hyt/approve/club/star-list.vue
  70. 703 0
      pages/_template/hyt/approve/device/detail.vue
  71. 377 0
      pages/_template/hyt/approve/device/index.vue
  72. 300 0
      pages/_template/hyt/approve/personnel/operate/detail.vue
  73. 282 0
      pages/_template/hyt/approve/personnel/operate/index.vue
  74. 468 0
      pages/_template/hyt/center/club-detail.vue
  75. 252 0
      pages/_template/hyt/center/device/detail.vue
  76. 275 0
      pages/_template/hyt/center/device/index.vue
  77. 472 0
      pages/_template/hyt/center/index.vue
  78. 295 0
      pages/_template/hyt/center/settings/password.vue
  79. 46 0
      pages/_template/hyt/center/subnav/account.vue
  80. 355 0
      pages/_template/hyt/docs/_fileId.vue
  81. 83 0
      pages/_template/hyt/docs/article-detail.vue
  82. 242 0
      pages/_template/hyt/docs/detail.vue
  83. 221 0
      pages/_template/hyt/feedback/index.vue
  84. 514 0
      pages/_template/hyt/form/club-bind.vue
  85. 527 0
      pages/_template/hyt/form/club-register.vue
  86. 685 0
      pages/_template/hyt/form/components/form-club-device.vue
  87. 845 0
      pages/_template/hyt/form/components/form-club-info.vue
  88. 226 0
      pages/_template/hyt/form/components/form-club-register.vue
  89. 271 0
      pages/_template/hyt/form/link-register.vue
  90. 564 0
      pages/_template/hyt/index.vue
  91. 398 0
      pages/_template/hyt/record/club/detail.vue
  92. 272 0
      pages/_template/hyt/record/club/edit.vue
  93. 393 0
      pages/_template/hyt/record/device/detail.vue
  94. 256 0
      pages/_template/hyt/record/device/edit.vue
  95. 315 0
      pages/_template/hyt/record/device/index.vue
  96. 112 0
      pages/_template/hyt/record/message.vue
  97. 1 1
      pages/_template/ph/feedback/index.vue
  98. 1 1
      pages/_template/ross/feedback/index.vue

BIN
assets/theme-images/hyt/h5/banner-club.png


BIN
assets/theme-images/hyt/h5/banner-device.png


BIN
assets/theme-images/hyt/h5/banner-doc.png


BIN
assets/theme-images/hyt/h5/banner-doctor.png


BIN
assets/theme-images/hyt/h5/banner-feedback.png


BIN
assets/theme-images/hyt/h5/banner-record.png


BIN
assets/theme-images/hyt/h5/banner-register.png


BIN
assets/theme-images/hyt/h5/club-info-bg.png


BIN
assets/theme-images/hyt/h5/h5-icon-mobile.png


BIN
assets/theme-images/hyt/h5/icon-address.png


BIN
assets/theme-images/hyt/h5/icon-auth-ren.png


BIN
assets/theme-images/hyt/h5/icon-auth-seal.png


BIN
assets/theme-images/hyt/h5/icon-auth.png


BIN
assets/theme-images/hyt/h5/icon-auth2.png


BIN
assets/theme-images/hyt/h5/icon-avatar-v.png


BIN
assets/theme-images/hyt/h5/icon-club-logo-default.png


BIN
assets/theme-images/hyt/h5/icon-device-list.png


BIN
assets/theme-images/hyt/h5/icon-feedback-submit.png


BIN
assets/theme-images/hyt/h5/icon-navigation.png


BIN
assets/theme-images/hyt/h5/link-entry-doc-active.png


BIN
assets/theme-images/hyt/h5/link-entry-feedback-active.png


BIN
assets/theme-images/hyt/h5/link-entry-register-active.png


BIN
assets/theme-images/hyt/h5/nav-entry-device-active.png


BIN
assets/theme-images/hyt/h5/nav-entry-doctor-active.png


BIN
assets/theme-images/hyt/logo.png


BIN
assets/theme-images/hyt/pc/banner-club.jpg


BIN
assets/theme-images/hyt/pc/banner-device.png


BIN
assets/theme-images/hyt/pc/banner-doc.png


BIN
assets/theme-images/hyt/pc/banner-doctor.png


BIN
assets/theme-images/hyt/pc/banner-feedback.png


BIN
assets/theme-images/hyt/pc/banner-record.png


BIN
assets/theme-images/hyt/pc/banner-register.png


BIN
assets/theme-images/hyt/pc/club-info-bg.png


BIN
assets/theme-images/hyt/pc/icon-address.png


BIN
assets/theme-images/hyt/pc/icon-auth-ren.png


BIN
assets/theme-images/hyt/pc/icon-auth-seal.png


BIN
assets/theme-images/hyt/pc/icon-auth.png


BIN
assets/theme-images/hyt/pc/icon-auth2.png


BIN
assets/theme-images/hyt/pc/icon-avatar-v.png


BIN
assets/theme-images/hyt/pc/icon-center-item-auth-club.png


BIN
assets/theme-images/hyt/pc/icon-center-item-device.png


BIN
assets/theme-images/hyt/pc/icon-club-logo-default.png


BIN
assets/theme-images/hyt/pc/icon-device-list.png


BIN
assets/theme-images/hyt/pc/icon-download-hover.png


BIN
assets/theme-images/hyt/pc/icon-feedback-submit.png


BIN
assets/theme-images/hyt/pc/icon-mobile.png


BIN
assets/theme-images/hyt/pc/icon-navigation.png


BIN
assets/theme-images/hyt/pc/link-entry-doc-active.png


BIN
assets/theme-images/hyt/pc/link-entry-doc.png


BIN
assets/theme-images/hyt/pc/link-entry-feedback-active.png


BIN
assets/theme-images/hyt/pc/link-entry-feedback.png


BIN
assets/theme-images/hyt/pc/link-entry-register-active.png


BIN
assets/theme-images/hyt/pc/link-entry-register.png


BIN
assets/theme-images/hyt/pc/logo.png


BIN
assets/theme-images/hyt/pc/nav-entry-device-active.png


BIN
assets/theme-images/hyt/pc/nav-entry-device.png


BIN
assets/theme-images/hyt/pc/nav-entry-doctor-active.png


BIN
assets/theme-images/hyt/pc/nav-entry-doctor.png


+ 2 - 0
assets/themes/themeVariable.scss

@@ -1,9 +1,11 @@
 @import './variables/normal.scss';
 @import './variables/ross.scss';
 @import './variables/ph.scss';
+@import './variables/hyt.scss';
 
 $themes: (
   normal: $normalTuple,
   ph: $phTuple,
   ross: $rossTuple,
+  hyt: $hytTuple,
 );

+ 6 - 0
assets/themes/variables/hyt.scss

@@ -0,0 +1,6 @@
+$hytTuple: (
+  color: #4093b5,
+  sub-color: #d5e3e9,
+  hover-color: #308aad,
+  cover-color: linear-gradient(180deg, #4093b5, #4bb9e6),
+);

+ 1 - 1
assets/themes/variables/normal.scss

@@ -2,5 +2,5 @@ $normalTuple: (
   color: #bc1724,
   sub-color: #ffe6e8,
   hover-color: #960915,
-  cover-color: linear-gradient(180deg, #ffe6e8 0%, rgba(255, 255, 255, 0) 100%),
+  cover-color: linear-gradient(180deg, #88111a 0%, #bc1724),
 );

+ 3 - 2
components/SimpleCenterCover/index.vue

@@ -21,8 +21,9 @@ export default {}
 
 <style scoped lang="scss">
 .area {
-  background: #88111a;
-  background: -webkit-linear-gradient(to left, #88111a, #bc1724);
+  @include themify($themes) {
+    background: themed('cover-color');
+  }
   width: 100%;
   height: 100%;
 }

+ 28 - 1
components/SimpleLogin/index.vue

@@ -47,7 +47,13 @@
                 v-model="formData.confirmPwd"
               />
             </div>
-            <div class="submit" @click="onSubmit">{{ submitText }}</div>
+            <div
+              class="submit"
+              @click="onSubmit"
+              :class="{ disabled: disabled }"
+            >
+              {{ submitText }}
+            </div>
             <div
               class="flex justify-between control mt-2"
               v-if="formType === 'login'"
@@ -102,6 +108,17 @@ export default {
         ? '确定'
         : '提交'
     },
+    disabled() {
+      return (
+        (this.formType === 'login' &&
+          (!this.formData.mobile || !this.formData.password)) ||
+        ((this.formType === 'register' || this.formType === 'forget') &&
+          (!this.formData.mobile ||
+            !this.formData.password ||
+            !this.formData.verifyCode ||
+            !this.formData.confirmPwd))
+      )
+    },
   },
   created() {
     this.formType = this.type
@@ -391,6 +408,11 @@ export default {
         cursor: pointer;
         border-radius: 4px;
 
+        &.disabled {
+          opacity: 0.5;
+          cursor: not-allowed;
+        }
+
         @include themify($themes) {
           background: themed('color');
         }
@@ -495,6 +517,11 @@ export default {
         transition: all 0.4s;
         border-radius: 4px;
 
+        &.disabled {
+          opacity: 0.5;
+          cursor: not-allowed;
+        }
+
         @include themify($themes) {
           background: themed('color');
         }

+ 4 - 0
configs/mode-map.js

@@ -12,4 +12,8 @@ export default [
     authUserId: 4,
     prefix: 'ph',
   },
+  {
+    authUserId: 15,
+    prefix: 'hyt',
+  },
 ]

+ 634 - 0
layouts/app-hyt.vue

@@ -0,0 +1,634 @@
+<template>
+  <div :class="themeClass" v-if="isMounted">
+    <div class="layout" :style="{ paddingTop: showHeader ? '' : 0 }">
+      <div class="header" v-show="showHeader">
+        <div class="navbar flex justify-between items-center">
+          <div class="logo flex items-center" @click="backHome">
+            <img src="~/assets/theme-images/hyt/logo.png" />
+            <span>认证通</span>
+          </div>
+          <div class="flex justify-center items-center">
+            <div class="nav" v-if="isPc">
+              <template v-for="item in list">
+                <div class="link" :key="item.id" @click="onJumpTo(item)">
+                  <span class="icon" :class="item.icon"></span>
+                  <span class="text">{{ item.name }}</span>
+                </div>
+              </template>
+            </div>
+            <div class="user-info">
+              <template v-if="accessToken">
+                <div class="user-center">
+                  <span class="icon el-icon-user-solid"></span>
+                  <span class="icon el-icon-arrow-down"></span>
+                  <div class="drop-down">
+                    <ul class="nav">
+                      <li @click.stop="onUserCenter">个人中心</li>
+                      <li @click.stop="logout">退出登录</li>
+                    </ul>
+                  </div>
+                </div>
+              </template>
+              <template v-else>
+                <div class="login-btn" @click="onLogin">登录</div>
+              </template>
+            </div>
+            <span class="collapse-icon" @click="drawer = true"></span>
+          </div>
+        </div>
+      </div>
+      <div class="content">
+        <nuxt />
+      </div>
+      <!-- <div class="footer flex justify-center items-center" v-show="showFooter">
+        - {{ supplierInfo.shopName }} | 由采美网提供技术支持 -
+      </div> -->
+      <SimpleLogin :type="formType" @click="onLoginClick"></SimpleLogin>
+    </div>
+
+    <template v-if="!isPc">
+      <el-drawer :visible.sync="drawer" size="63%">
+        <div class="nav">
+          <template v-for="item in list">
+            <div class="link" :key="item.id" @click="onJumpTo(item)">
+              <span class="icon" :class="item.icon"></span>
+              <span class="text">{{ item.name }}</span>
+            </div>
+          </template>
+        </div>
+      </el-drawer>
+    </template>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  computed: {
+    ...mapGetters([
+      'userInfo',
+      'accessToken',
+      'authUserId',
+      'appId',
+      'routePrefix',
+      'themeName',
+      'isPc',
+      'showHeader',
+      'showFooter',
+      'supplierInfo',
+    ]),
+    themeClass() {
+      return `theme-${this.themeName}`
+    },
+  },
+  head() {
+    return {
+      meta: [
+        {
+          name: 'viewport',
+          content:
+            'width=device-width,initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0, user-scalable=no',
+        },
+      ],
+    }
+  },
+  data() {
+    return {
+      formType: 'login',
+      drawer: false,
+      isMounted: false,
+      list: [
+        {
+          id: 1,
+          name: '授权申请',
+          path: '/form/club-register',
+          icon: 'icon-register',
+        },
+        {
+          id: 2,
+          name: '云资料库',
+          path: '/docs/0',
+          icon: 'icon-doc',
+        },
+        {
+          id: 3,
+          name: '意见反馈',
+          path: '/feedback',
+          icon: 'icon-feedback',
+        },
+      ],
+    }
+  },
+  created() {},
+  mounted() {
+    this.responseWidth()
+    this.initPageData()
+    // this.checkAccountType()
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', () => {})
+    this.refreshCacheData()
+  },
+  methods: {
+    // 跳转
+    onJumpTo(item) {
+      this.drawer = false
+      const hasLogin = this.$store.getters.accessToken
+      // 保存登录重定向路由
+      this.$setStorage(
+        this.routePrefix,
+        'login_redicret',
+        this.routePrefix + item.path
+      )
+      if (item.id > 2 && !hasLogin) {
+        this.$toast({ message: '请先登录', duration: 1000 })
+        this.formType = 'login'
+        this.$store.commit('app/SHOW_LOGIN')
+        return
+      }
+
+      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)
+      }
+    },
+
+    // 点击登录
+    onLoginClick(type) {
+      this.formType = type
+    },
+
+    // 初始化数据页面公共数据
+    async initPageData() {
+      this.$store.commit('app/SET_PAGE_THEME', 'hyt')
+      // 获取用户信息
+      let userInfo = this.$getStorage(this.routePrefix, 'userInfo')
+      if (userInfo && userInfo.authUserId === this.authUserId) {
+        this.$store.commit('user/SET_USER_INFO', userInfo)
+        const res = await this.$http.api.checkTokenResult()
+        this.$store.commit('user/SET_USER_INFO', res.data)
+      }
+      this.isMounted = true
+    },
+
+    // 校验公众号类型
+    async checkAccountType() {
+      try {
+        // 1订阅号,2服务号
+        if (!this.appId) return
+        const res = await this.$http.api.checkAccountType({ appId: this.appId })
+        this.$store.commit('supplier/SET_ACCOUNT_TYPE', res.data)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 登录
+    onLogin() {
+      this.formType = 'login'
+      this.$store.commit('app/SHOW_LOGIN')
+    },
+
+    // 注册
+    onRegister() {
+      this.formType = 'register'
+      this.$store.commit('app/SHOW_LOGIN')
+    },
+
+    // 退出登录
+    logout() {
+      this.$store.dispatch('user/logout')
+      this.$removeStorage(this.routePrefix, 'userInfo')
+      this.backHome()
+    },
+
+    // 回到首页
+    backHome() {
+      // if (this.$route.path === this.routePrefix) return
+      // this.$router.replace(this.routePrefix)
+      window.location.href = window.location.origin + this.routePrefix
+    },
+
+    // 个人中心
+    onUserCenter() {
+      // const path = `${this.routePrefix}/center`
+      window.location.href =
+        window.location.origin + `${this.routePrefix}/center`
+    },
+
+    // 响应页面宽度变化
+    responseWidth() {
+      this.$store.commit('app/SET_SCREEN', window.innerWidth)
+      window.addEventListener('resize', (e) => {
+        this.$store.commit('app/SET_SCREEN', e.target.innerWidth)
+      })
+    },
+
+    // 数据初始化刷新浏览器
+    refreshCacheData() {
+      this.$removeStorage(this.routePrefix, 'club_list_data')
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+@keyframes slide-down {
+  0% {
+    top: 48px;
+    z-index: 9;
+    opacity: 0;
+  }
+  100% {
+    top: 32px;
+    opacity: 1;
+  }
+}
+
+// PC端
+@media screen and (min-width: 768px) {
+  .layout {
+    padding-top: 80px;
+    user-select: none;
+
+    .header {
+      position: fixed;
+      top: 0;
+      left: 0;
+      z-index: 999;
+      width: 100%;
+      height: 80px;
+      box-sizing: border-box;
+      background: linear-gradient(90deg, #101010 0%, #404040 100%);
+
+      .navbar {
+        width: 1200px;
+        margin: 0 auto;
+        height: 100%;
+      }
+
+      .logo {
+        cursor: pointer;
+        img {
+          display: block;
+          width: 122px;
+          height: 44px;
+
+          &.ross {
+            width: 85px;
+            height: 27px;
+            margin-right: 15px;
+            transform: translateY(-2px);
+          }
+        }
+        span {
+          font-size: 24px;
+          color: #fff;
+          margin-left: 8px;
+        }
+      }
+
+      .nav {
+        .link {
+          display: inline;
+          margin-left: 32px;
+          cursor: pointer;
+
+          &:hover {
+            .text {
+              @include themify($themes) {
+                color: themed('color');
+              }
+            }
+
+            .icon {
+              &.icon-register {
+                background-image: url(~assets/theme-images/hyt/pc/link-entry-register-active.png);
+              }
+              &.icon-doc {
+                background-image: url(~assets/theme-images/hyt/pc/link-entry-doc-active.png);
+              }
+              &.icon-feedback {
+                background-image: url(~assets/theme-images/hyt/pc/link-entry-feedback-active.png);
+              }
+            }
+          }
+        }
+
+        .icon {
+          width: 20px;
+          height: 20px;
+          display: inline-block;
+          vertical-align: -4px;
+          margin-right: 4px;
+
+          background-size: 20px;
+          background-repeat: no-repeat;
+          background-position: center;
+
+          &.icon-register {
+            background-image: url(~assets/theme-images/hyt/pc/link-entry-register.png);
+          }
+          &.icon-doc {
+            background-image: url(~assets/theme-images/hyt/pc/link-entry-doc.png);
+          }
+          &.icon-feedback {
+            background-image: url(~assets/theme-images/hyt/pc/link-entry-feedback.png);
+          }
+        }
+        .text {
+          font-size: 16px;
+          color: #fff;
+        }
+      }
+
+      .user-info {
+        color: #fff;
+        font-size: 16px;
+        margin-left: 48px;
+
+        .login-btn {
+          width: 80px;
+          height: 34px;
+          color: #fff;
+          background: rgba(255, 255, 255, 0.39);
+          font-size: 14px;
+          text-align: center;
+          line-height: 34px;
+          cursor: pointer;
+        }
+        .user-center {
+          position: relative;
+
+          .icon {
+            width: 32px;
+            height: 32px;
+            text-align: center;
+            line-height: 32px;
+
+            &.el-icon-user-solid {
+              font-size: 24px;
+              cursor: pointer;
+            }
+
+            &.el-icon-arrow-down {
+              font-size: 22px;
+              transition: all 0.2s;
+            }
+          }
+
+          &:hover {
+            .drop-down {
+              display: block;
+              animation: slide-down 0.4s linear forwards;
+            }
+
+            .el-icon-arrow-down {
+              transform: rotateZ(180deg);
+            }
+          }
+
+          .drop-down {
+            display: none;
+            opacity: 0;
+            // z-index: -1;
+            right: 0;
+
+            position: absolute;
+            background: transparent;
+            box-sizing: border-box;
+            padding-top: 24px;
+
+            .nav {
+              width: 118px;
+              padding: 8px 0;
+              background: #fff;
+              box-shadow: 0px 6px 16px rgba(40, 40, 40, 0.1);
+              border-radius: 4px;
+              li {
+                font-size: 14px;
+                color: #282828;
+                text-align: center;
+                line-height: 40px;
+                transition: all 0.4s;
+                cursor: pointer;
+
+                &:hover {
+                  @include themify($themes) {
+                    color: themed('color');
+                  }
+                }
+              }
+            }
+          }
+        }
+
+        .login,
+        .register,
+        .logout {
+          cursor: pointer;
+
+          &:hover {
+            @include themify($themes) {
+              color: themed('color');
+            }
+          }
+        }
+      }
+    }
+    .content {
+      // min-height: calc(100vh - 80px - 80px);
+      min-height: calc(100vh - 80px);
+      background-color: #f7f7f7;
+      overflow: hidden;
+    }
+
+    .footer {
+      height: 80px;
+      background-color: #2c3038;
+      color: #fff;
+      font-size: 14px;
+    }
+  }
+}
+
+// 移动端
+@media screen and (max-width: 768px) {
+  .layout {
+    padding-top: 12.8vw;
+    .header {
+      position: fixed;
+      top: 0;
+      left: 0;
+      z-index: 999;
+      width: 100%;
+      padding: 0 4vw;
+      height: 12.8vw;
+      box-sizing: border-box;
+      background: linear-gradient(90deg, #101010 0%, #404040 100%);
+
+      .navbar {
+        height: 100%;
+      }
+
+      .logo {
+        img {
+          display: block;
+          width: 22.5vw;
+          height: 8vw;
+
+          // &.ross {
+          //   width: 12.2vw;
+          //   height: 3.9vw;
+          //   margin-right: 1.9vw;
+          //   transform: translateY(-0.6vw);
+          // }
+        }
+        span {
+          font-size: 4vw;
+          color: #fff;
+          display: none;
+        }
+      }
+
+      .user-info {
+        color: #fff;
+        font-size: 3vw;
+
+        .logout {
+          margin: 0 1.6vw;
+        }
+
+        .login-btn {
+          width: 13.4vw;
+          height: 6.4vw;
+          color: #fff;
+          background: rgba(255, 255, 255, 0.39);
+          font-size: 3.4vw;
+          text-align: center;
+          line-height: 6.4vw;
+          margin-right: 2.4vw;
+        }
+
+        .user-center {
+          position: relative;
+
+          .icon {
+            width: 5.6vw;
+            height: 5.6vw;
+            text-align: center;
+            line-height: 5.6vw;
+            margin-right: 4vw;
+
+            &.el-icon-user-solid {
+              font-size: 24px;
+              cursor: pointer;
+            }
+
+            &.el-icon-arrow-down {
+              display: none;
+            }
+          }
+
+          &:hover {
+            .drop-down {
+              display: block;
+              animation: slide-down 0.4s linear forwards;
+            }
+
+            .el-icon-arrow-down {
+              transform: rotateZ(180deg);
+            }
+          }
+
+          .drop-down {
+            display: none;
+            opacity: 0;
+            right: 0;
+
+            position: absolute;
+            background: transparent;
+            box-sizing: border-box;
+            padding-top: 1.2vw;
+
+            .nav {
+              width: 26vw;
+              padding: 1vw 0;
+              background: #fff;
+              box-shadow: 0px 0.6vw 20vw rgba(40, 40, 40, 0.1);
+              border-radius: 0.4vw;
+              li {
+                font-size: 3.4vw;
+                color: #282828;
+                text-align: center;
+                line-height: 8.6vw;
+                transition: all 0.4s;
+              }
+            }
+          }
+        }
+      }
+
+      .collapse-icon {
+        display: block;
+        width: 5.6vw;
+        height: 5.6vw;
+        background: url(~assets/theme-images/common/h5-icon-collapse.png)
+          no-repeat center;
+        background-size: 5.6vw;
+      }
+    }
+    .content {
+      // min-height: calc(100vh - 12.8vw - 12.4vw);
+      min-height: calc(100vh - 12.8vw);
+    }
+
+    .footer {
+      height: 12.4vw;
+      background-color: #2c3038;
+      color: #fff;
+      font-size: 3vw;
+    }
+  }
+
+  .nav {
+    width: 63vw;
+    box-sizing: border-box;
+    padding: 0 6.4vw;
+    .link {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      border-bottom: 0.1vw solid #c2c2c2;
+      padding-bottom: 3vw;
+      padding-top: 6vw;
+      .icon {
+        width: 5.6vw;
+        height: 5.6vw;
+        vertical-align: -1.2vw;
+        margin-right: 2.4vw;
+        background-size: 5.6vw;
+        background-repeat: no-repeat;
+        background-position: center;
+
+        &.icon-register {
+          background-image: url(~assets/theme-images/hyt/h5/link-entry-register-active.png);
+        }
+        &.icon-doc {
+          background-image: url(~assets/theme-images/hyt/h5/link-entry-doc-active.png);
+        }
+        &.icon-feedback {
+          background-image: url(~assets/theme-images/hyt/h5/link-entry-feedback-active.png);
+        }
+      }
+      .text {
+        font-size: 3.4vw;
+        color: #282828;
+      }
+    }
+  }
+}
+</style>

+ 1 - 1
middleware/intercept.js

@@ -77,7 +77,7 @@ async function initTemplateEntry(context) {
       }
 
       // 非默认模板
-      if (['ross', 'ldm', 'ph'].includes(prefix)) {
+      if (['ross', 'ldm', 'ph', 'hyt'].includes(prefix)) {
         // 供应商为指定模板
         if (!isMatchTemplate(authUserId, prefix)) {
           showError(error, { statusCode: 500, message: '模板与供应商不匹配' })

+ 1 - 1
pages/_template/app/feedback/index.vue

@@ -13,7 +13,7 @@
     <van-dialog v-model="showModal" class="dialog" @confirm="onConfirm">
       <div class="dialog-content">
         <div class="image-icon"></div>
-        <div class="title">提成功</div>
+        <div class="title">提成功</div>
         <div class="tip">您的反馈信息已提交,感谢您的宝贵意见。</div>
         <div class="line" />
       </div>

+ 672 - 0
pages/_template/hyt/approve/club/detail.vue

@@ -0,0 +1,672 @@
+<template>
+  <div class="page md:flex md:justify-between">
+    <div class="page-title">机构认证</div>
+    <div class="page-top">
+      <div class="swiper">
+        <SimpleSwiper :imageList="clubInfo.bannerList"></SimpleSwiper>
+        <img
+          class="auth-card"
+          :src="authCardImage"
+          @click="onShowAuthCard"
+          v-if="!showAuthCard"
+        />
+      </div>
+    </div>
+    <div class="page-content">
+      <div class="bg-container">
+        <!-- 机构信息 -->
+        <div class="club-info">
+          <div class="section flex justify-between items-center">
+            <div class="info">
+              <div class="name" v-text="clubInfo.authParty"></div>
+              <div class="mobile" @click="callMobile(clubInfo.mobile)">
+                {{ clubInfo.mobile | formatEmpty }}
+              </div>
+              <div class="address" v-text="address"></div>
+            </div>
+            <div class="logo"><img :src="clubInfo.logo" /></div>
+          </div>
+          <div class="section flex justify-between items-center mt-6">
+            <div class="navigation" @click="onMapNav">导航</div>
+            <div
+              class="distance"
+              v-if="clubInfo.distance && clubInfo.distance < 99999"
+              v-text="'距你' + clubInfo.distance + 'km'"
+            ></div>
+          </div>
+        </div>
+        <!-- 机构授权信息 -->
+        <div class="auth">
+          <div class="auth-icon"></div>
+          <div class="auth-info">
+            <span>该机构由</span>
+            <span class="font-bold">{{ supplierInfo.shopName }}</span>
+            <span class="font-bold">官方授权</span>
+          </div>
+        </div>
+      </div>
+      <!-- 分割 -->
+      <div class="divider"></div>
+      <div class="device-list">
+        <div class="title">已认证设备</div>
+        <div class="list">
+          <div
+            class="device flex justify-between items-center"
+            v-for="item in clubInfo.productList"
+            :key="item.productId"
+          >
+            <div class="info">
+              <div class="name" v-text="item.productName"></div>
+              <div class="code">SN码:{{ item.snCode | formatSnCode }}</div>
+            </div>
+            <div class="detail" @click="toDetail(item)">查看认证</div>
+          </div>
+        </div>
+      </div>
+      <SimpleEmpty
+        v-if="isEmpty"
+        name="icon-empty-device.png"
+        description="暂无已认证设备"
+      ></SimpleEmpty>
+    </div>
+
+    <SimpleMapNav
+      ref="mapNav"
+      @click="navigation"
+      color="#4093B5"
+    ></SimpleMapNav>
+
+    <!-- 授权牌弹窗 -->
+    <div class="mask" v-if="showAuthCard" @click="onHideAuthCard"></div>
+    <transition
+      enter-active-class="animate__zoomIn"
+      leave-active-class="animate__zoomOut"
+    >
+      <div class="auth-card-content animate__animated" v-if="showAuthCard">
+        <div class="auth-card-popup">
+          <span class="el-icon-circle-close" @click="onHideAuthCard"></span>
+          <img :src="authCardImage" />
+        </div>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script>
+import clubDetailMixin from '@/mixins/clubDetail'
+export default {
+  layout: 'app-hyt',
+  mixins: [clubDetailMixin],
+}
+</script>
+
+<style scoped lang="scss">
+.mask {
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  left: 0;
+  top: 0;
+  z-index: 8;
+  background: rgba(0, 0, 0, 0.5);
+}
+
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    width: 1200px;
+    height: 612px;
+    margin-left: auto;
+    margin-right: auto;
+    margin-top: 80px;
+    background-color: #fff;
+    box-sizing: border-box;
+    padding: 16px;
+    padding-right: 0;
+  }
+
+  .auth-card-content {
+    width: 100vw;
+    height: 100vh;
+    position: fixed;
+    left: 0;
+    top: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 9;
+    .auth-card-popup {
+      position: relative;
+      width: 622px;
+
+      img {
+        display: block;
+        width: 100%;
+        height: auto;
+      }
+
+      .el-icon-circle-close {
+        position: absolute;
+        top: -50px;
+        right: 0;
+        font-size: 32px;
+        color: #fff;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .page-title {
+    position: absolute;
+    font-size: 24px;
+    color: #333;
+    top: -50px;
+    left: 0;
+  }
+
+  .page-top {
+    margin-right: 24px;
+    .swiper {
+      position: relative;
+      width: 580px;
+      height: 580px;
+      background: #f7f7f7;
+      ::v-deep {
+        img {
+          width: 580px;
+          height: 580px;
+        }
+      }
+
+      .auth-card {
+        position: absolute;
+        width: auto;
+        height: 110px;
+        display: block;
+        bottom: 24px;
+        left: 24px;
+        z-index: 2;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .page-content {
+    width: 580px;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar {
+      width: 8px;
+      // background-color: #eeeeee;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      border-radius: 4px;
+      background-color: #eeeeee;
+    }
+
+    .bg-container {
+      // background: url(~assets/theme-images/hyt/pc/club-info-bg.png) center
+      //   no-repeat;
+      background-size: 100%;
+      box-sizing: border-box;
+      background-position-y: 20px;
+      background-color: #76AFCC;
+      padding-bottom: 24px;
+    }
+
+    .auth {
+      width: 524px;
+      box-sizing: border-box;
+      background: #FFFFFF;
+      margin: 0 auto;
+      margin-top: 8px;
+      padding: 12px 16px;
+      border-radius: 2px;
+
+      .auth-icon {
+        height: 28px;
+        background: url(~assets/theme-images/hyt/pc/icon-auth.png) no-repeat
+          left center;
+        background-size: auto 28px;
+      }
+
+      .auth-info {
+        font-size: 0;
+        margin-top: 10px;
+        span {
+          font-size: 20px;
+          line-height: 1.6;
+          color: #4093B5;
+
+          &.font-bold {
+            font-weight: bold;
+          }
+        }
+      }
+    }
+
+    .club-info {
+      padding: 32px 24px;
+      width: 564px;
+      min-height: 264px;
+
+      .info {
+        width: 360px;
+        .name {
+          font-size: 24px;
+          color: #fff;
+          font-weight: bold;
+          margin-bottom: 34px;
+        }
+        .mobile,
+        .address {
+          position: relative;
+          padding-left: 28px;
+          margin-top: 16px;
+          line-height: 24px;
+          font-size: 16px;
+          color: #fff;
+
+          &::after {
+            content: '';
+            display: block;
+            width: 24px;
+            height: 24px;
+            position: absolute;
+            left: 0;
+            top: 0;
+            background-size: 20px;
+            background-repeat: no-repeat;
+            background-position: center;
+          }
+        }
+        .mobile {
+          cursor: pointer;
+          &::after {
+            background-image: url(~assets/theme-images/hyt/pc/icon-mobile.png);
+          }
+        }
+        .address {
+          &::after {
+            background-image: url(~assets/theme-images/hyt/pc/icon-address.png);
+          }
+        }
+      }
+
+      .logo {
+        position: relative;
+        width: 114px;
+        height: 114px;
+        border-radius: 50% 50% 0 50%;
+        overflow: hidden;
+        background: #fff;
+        display: flex;
+
+        &::after {
+          position: absolute;
+          bottom: 0;
+          right: 0;
+          content: '';
+          display: block;
+          width: 23px;
+          height: 23px;
+          background: url(~assets/theme-images/hyt/pc/icon-avatar-v.png)
+            no-repeat center;
+          background-size: 23px;
+        }
+      }
+      .navigation {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 72px;
+        height: 32px;
+        border-radius: 4px;
+
+        font-size: 16px;
+        color: #4093B5;
+        border: 1px solid #fff;
+        background-color: #fff;
+        cursor: pointer;
+
+        &::after {
+          content: '';
+          display: block;
+          width: 16px;
+          height: 16px;
+          margin-left: 4px;
+          background: url(~assets/theme-images/hyt/pc/icon-navigation.png)
+            no-repeat center;
+          background-size: 16px;
+        }
+      }
+      .distance {
+        font-size: 14px;
+        color: #fff;
+      }
+    }
+
+    .device-list {
+      .title {
+        position: relative;
+        font-size: 20px;
+        font-weight: bold;
+        color: #282828;
+        line-height: 30px;
+        padding-left: 32px;
+        margin-top: 46px;
+        margin-bottom: 24px;
+
+        &::before {
+          content: '';
+          display: block;
+          width: 30px;
+          height: 30px;
+          background: url(~assets/theme-images/hyt/pc/icon-device-list.png)
+            no-repeat center;
+          position: absolute;
+          left: 0;
+          top: 0;
+        }
+      }
+
+      .list {
+        padding-right: 16px;
+      }
+
+      .device {
+        padding: 20px 16px;
+        box-sizing: border-box;
+        background: #f3f5f6;
+        margin-top: 16px;
+
+        &:first-child {
+          margin-top: 0;
+        }
+
+        .info {
+          width: 80%;
+          .name {
+            font-size: 18px;
+            color: #282828;
+          }
+          .code {
+            margin-top: 12px;
+            font-size: 14px;
+            color: #666;
+          }
+        }
+        .detail {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          width: 80px;
+          height: 32px;
+          border-radius: 4px;
+          font-size: 14px;
+          color: #ffffff;
+          cursor: pointer;
+          background: #4093B5;
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-title {
+    display: none;
+  }
+
+  .auth-card-content {
+    width: 100vw;
+    height: 100vh;
+    position: fixed;
+    left: 0;
+    top: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 9;
+    .auth-card-popup {
+      position: relative;
+      width: 86vw;
+
+      img {
+        display: block;
+        width: 100%;
+        height: auto;
+      }
+
+      .el-icon-circle-close {
+        position: absolute;
+        top: -8vw;
+        right: 0;
+        font-size: 7vw;
+        color: #fff;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .page-top {
+    .swiper {
+      position: relative;
+      height: 100vw;
+
+      background: #f7f7f7;
+      ::v-deep {
+        img {
+          height: 100vw;
+        }
+      }
+
+      .auth-card {
+        position: absolute;
+        width: auto;
+        height: 20.6vw;
+        display: block;
+        bottom: 4vw;
+        left: 4vw;
+        z-index: 2;
+      }
+    }
+  }
+
+  .page-content {
+    .bg-container {
+      // background: url(~assets/theme-images/hyt/h5/club-info-bg.png) center
+      //   no-repeat;
+      background-size: 100vw;
+      box-sizing: border-box;
+      background-position-y: 4vw;
+      background-color: #76AFCC;
+      padding-bottom: 5vw;
+    }
+
+    .auth {
+      width: 92vw;
+      box-sizing: border-box;
+      background: #fff;
+      margin: 0 auto;
+      margin-top: 3.2vw;
+      padding: 4vw 3.5vw;
+      border-radius: 0.8vw;
+
+      .auth-icon {
+        height: 6.2vw;
+        background: url(~assets/theme-images/hyt/h5/icon-auth.png) no-repeat
+          left center;
+        background-size: auto 6.2vw;
+      }
+
+      .auth-info {
+        font-size: 0;
+        margin-top: 2.2vw;
+        span {
+          font-size: 3.6vw;
+          line-height: 5.8vw;
+          color: #4093B5;
+
+          &.font-bold {
+            font-weight: bold;
+          }
+        }
+      }
+    }
+    .club-info {
+      padding: 4vw;
+      width: 100vw;
+      min-height: 43.4vw;
+
+      .info {
+        width: 67vw;
+        .name {
+          font-size: 4.8vw;
+          color: #fff;
+          font-weight: bold;
+          margin-bottom: 4vw;
+        }
+        .mobile,
+        .address {
+          position: relative;
+          padding-left: 5vw;
+          margin-top: 1.6vw;
+          line-height: 5vw;
+          font-size: 3.2vw;
+          color: #fff;
+
+          &::after {
+            content: '';
+            display: block;
+            width: 4vw;
+            height: 5vw;
+            position: absolute;
+            left: 0;
+            top: 0;
+            background-size: 4vw 4vw;
+            background-repeat: no-repeat;
+            background-position: center;
+          }
+        }
+        .mobile {
+          &::after {
+            background-image: url(~assets/theme-images/hyt/pc/icon-mobile.png);
+          }
+        }
+        .address {
+          &::after {
+            background-image: url(~assets/theme-images/hyt/pc/icon-address.png);
+          }
+        }
+      }
+
+      .logo {
+        position: relative;
+        width: 18vw;
+        height: 18vw;
+        border-radius: 9vw 9vw 0 9vw;
+        overflow: hidden;
+        background: #fff;
+        display: flex;
+
+        &::after {
+          position: absolute;
+          bottom: 0;
+          right: 0;
+          content: '';
+          display: block;
+          width: 3.6vw;
+          height: 3.6vw;
+          background: url(~assets/theme-images/hyt/h5/icon-avatar-v.png)
+            no-repeat center;
+          background-size: 3.6vw;
+        }
+      }
+      .navigation {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 14.4vw;
+        height: 6.4vw;
+        border-radius: 0.4vw;
+        font-size: 3.2vw;
+        color: #4093B5;
+        border: 1px solid #fff;
+        background-color: #fff;
+
+        &::after {
+          content: '';
+          display: block;
+          width: 3.6vw;
+          height: 3.6vw;
+          margin-left: 0.4vw;
+          background: url(~assets/theme-images/hyt/pc/icon-navigation.png)
+            no-repeat center;
+          background-size: 3.6vw;
+        }
+      }
+      .distance {
+        font-size: 3vw;
+        color: #fff;
+      }
+    }
+
+    .device-list {
+      padding: 0 4vw;
+      .title {
+        position: relative;
+        padding-left: 6.8vw;
+        margin-top: 9.6vw;
+        margin-bottom: 4vw;
+        font-size: 4vw;
+        font-weight: bold;
+        color: #101010;
+
+        &::before {
+          content: '';
+          display: block;
+          width: 4.4vw;
+          height: 4.4vw;
+          background: url(~assets/theme-images/hyt/h5/icon-device-list.png)
+            no-repeat center;
+          position: absolute;
+          left: 0;
+          top: 0;
+        }
+      }
+      .device {
+        padding: 4.4vw 3.2vw;
+        background: #f3f5f6;
+        margin-bottom: 3.2vw;
+        .info {
+          width: 74%;
+          .name {
+            font-size: 3.6vw;
+            color: #282828;
+          }
+          .code {
+            margin-top: 3.2vw;
+            font-size: 3vw;
+            color: #666;
+          }
+        }
+        .detail {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          width: 15.8vw;
+          height: 6.4vw;
+          border-radius: 0.4vw;
+          font-size: 3vw;
+          color: #ffffff;
+          background: #4093B5;
+        }
+      }
+    }
+  }
+}
+</style>

+ 432 - 0
pages/_template/hyt/approve/club/star-list.vue

@@ -0,0 +1,432 @@
+<template>
+  <div class="page">
+    <div class="page-content">
+      <!-- 标题 -->
+      <div class="title flex justify-between px-4 pt-8 pb-6 md:px-0">
+        <div>明星机构</div>
+        <div>共<span v-text="total" class="font-bold"></span>家明星机构</div>
+      </div>
+      <!-- 列表 -->
+      <div class="list">
+        <template v-for="item in list">
+          <div
+            class="section flex justify-between mb-4"
+            :key="item.authId"
+            @click="toDetail(item)"
+          >
+            <img class="cover" :src="item.logo || drawLogo(item.authParty)" />
+            <div class="info">
+              <div class="name" v-text="item.authParty"></div>
+              <div class="mobile">{{ item.mobile || '暂无' }}</div>
+              <div class="address">
+                {{ formatAddress(item.area, item.address) }}
+              </div>
+            </div>
+          </div>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import clubStarListMixin from '@/mixins/clubStarList'
+export default {
+  layout: 'app-hyt',
+  mixins: [clubStarListMixin],
+}
+</script>
+
+<style scoped lang="scss">
+.el-input {
+  ::v-deep {
+    & > {
+      .el-input.is-active .el-input__inner,
+      .el-input__inner:focus {
+        border-color: #4093B5;
+      }
+    }
+  }
+}
+
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+  }
+  .page-top {
+    height: 530px;
+    background-image: url(~assets/theme-images/hyt/pc/banner-club.jpg);
+    background-size: cover;
+    background-position: center;
+  }
+  .page-content {
+    position: relative;
+    width: 1000px;
+    margin: 0 auto;
+
+    .title {
+      font-size: 16px;
+      color: #404040;
+
+      span {
+        color: #4093B5;
+      }
+    }
+
+    .filter {
+      padding: 48px 0 105px;
+      .search {
+        width: 640px;
+        margin: 0 auto;
+        .el-input {
+          height: 46px;
+          font-size: 16px;
+          .el-input__icon {
+            font-size: 24px;
+            line-height: 46px;
+            margin-left: 12px;
+          }
+
+          ::v-deep {
+            & > .el-input__inner {
+              height: 46px;
+              padding-left: 55px;
+            }
+          }
+        }
+      }
+    }
+
+    .navbar {
+      position: absolute;
+      top: 240px;
+      right: -168px;
+      width: 120px;
+      border-radius: 16px;
+      background: #fff;
+      box-shadow: 0px 6px 20px rgba(40, 40, 40, 0.1);
+      padding: 24px 0;
+      box-sizing: border-box;
+      z-index: 2;
+      .link {
+        &:hover {
+          .icon {
+            &.icon-device {
+              background: url(~assets/theme-images/hyt/pc/nav-entry-device-active.png)
+                  no-repeat center center,
+                linear-gradient(180deg, #ffba63 0%, #4093B5 100%);
+              background-size: 48px, 100%;
+            }
+            &.icon-doctor {
+              background: url(~assets/theme-images/hyt/pc/nav-entry-doctor-active.png)
+                  no-repeat center center,
+                linear-gradient(180deg, #ffba63 0%, #4093B5 100%);
+              background-size: 48px, 100%;
+            }
+          }
+          .text {
+            color: #4093B5;
+          }
+        }
+
+        span {
+          display: block;
+        }
+
+        .icon {
+          width: 72px;
+          height: 72px;
+          // background: linear-gradient(180deg, #ffba63 0%, #BC1724 100%);
+          background-color: #f6f6f7;
+          border-radius: 12px;
+
+          &.icon-device {
+            background: url(~assets/theme-images/hyt/pc/nav-entry-device.png)
+              no-repeat center center #f6f6f7;
+            background-size: 48px;
+          }
+
+          &.icon-doctor {
+            background: url(~assets/theme-images/hyt/pc/nav-entry-doctor.png)
+              no-repeat center center #f6f6f7;
+            background-size: 48px;
+          }
+        }
+
+        .text {
+          font-size: 16px;
+          color: #404040;
+          margin-top: 8px;
+        }
+      }
+    }
+
+    .list {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      flex-wrap: wrap;
+
+      .empty {
+        width: 390px;
+      }
+
+      .section {
+        width: 490px;
+        height: 136px;
+        background-color: #f3f5f6;
+        border-radius: 4px;
+        box-sizing: border-box;
+        padding: 16px;
+        cursor: pointer;
+        transition: all 0.4s;
+        &:hover {
+          box-shadow: 0 0 24px rgba(0, 0, 0, 0.2);
+        }
+
+        .cover {
+          display: block;
+          width: 104px;
+          height: 104px;
+        }
+        .info {
+          position: relative;
+          margin-left: 12px;
+          width: 330px;
+          .name {
+            width: 200px;
+            font-size: 18px;
+            color: #101010;
+            font-weight: bold;
+            margin-bottom: 24px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+          }
+          .mobile,
+          .address {
+            width: 268px;
+            position: relative;
+            font-size: 14px;
+            color: #404040;
+            padding-left: 24px;
+            line-height: 24px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            margin-top: 6px;
+            overflow: hidden;
+            &::after {
+              content: '';
+              display: block;
+              width: 16px;
+              height: 16px;
+              position: absolute;
+              left: 0;
+              top: 50%;
+              transform: translateY(-50%);
+              background-size: 16px;
+              background-repeat: no-repeat;
+            }
+          }
+          .mobile {
+            &::after {
+              background-image: url(~assets/theme-images/common/pc-icon-mobile.png);
+            }
+          }
+          .address {
+            &::after {
+              background-image: url(~assets/theme-images/common/pc-icon-address.png);
+            }
+          }
+
+          .distance {
+            position: absolute;
+            font-size: 14px;
+            color: #404040;
+            top: 2px;
+            right: 0;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 100vw;
+    background: url(~assets/theme-images/hyt/h5/banner-club.png);
+    background-size: 100vw 100vw !important;
+    background-position: center;
+
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+  .page-content {
+    position: relative;
+
+    .title {
+      font-size: 3.4vw;
+      color: #404040;
+
+      span {
+        color: #4093B5;
+      }
+    }
+
+    .filter {
+      padding: 6.4vw 3.2vw 12.8vw;
+    }
+
+    .navbar {
+      position: fixed;
+      top: 50% !important;
+      right: 3.2vw;
+      left: unset !important;
+      width: 14vw;
+      border-radius: 1.6vw;
+      background: #fff;
+      box-shadow: 0px 0.6vw 2vw rgba(40, 40, 40, 0.1);
+      padding: 2.8vw 0;
+      box-sizing: border-box;
+      z-index: 2;
+      span {
+        display: block;
+      }
+
+      .icon {
+        position: relative;
+        width: 7.2vw;
+        height: 7.2vw;
+        border-radius: 1.2vw;
+        background: linear-gradient(180deg, #ffba63 0%, #4093B5 100%);
+
+        &.icon-device,
+        &.icon-doctor {
+          &::after {
+            content: '';
+            display: block;
+            width: 4.8vw;
+            height: 4.8vw;
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            background-size: 4.8vw !important;
+          }
+        }
+
+        &.icon-device {
+          &::after {
+            background: url(~assets/theme-images/hyt/pc/nav-entry-device-active.png)
+              no-repeat center;
+          }
+        }
+
+        &.icon-doctor {
+          &::after {
+            background: url(~assets/theme-images/hyt/pc/nav-entry-doctor-active.png)
+              no-repeat center;
+          }
+        }
+      }
+
+      .text {
+        font-size: 2.4vw;
+        color: #4093B5;
+        margin-top: 1.2vw;
+      }
+    }
+  }
+
+  .list {
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+
+    .section {
+      width: 93.6vw;
+      height: 26vw;
+      background-color: #f3f5f6;
+      border-radius: 4px;
+      box-sizing: border-box;
+      padding: 3.2vw;
+
+      .cover {
+        display: block;
+        width: 19.6vw;
+        height: 19.6vw;
+      }
+      .info {
+        position: relative;
+        margin-left: 3.2vw;
+        .name {
+          width: 48vw;
+          font-size: 4vw;
+          color: #101010;
+          font-weight: bold;
+          margin-bottom: 4vw;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          overflow: hidden;
+        }
+        .mobile,
+        .address {
+          width: 66vw;
+          position: relative;
+          font-size: 3vw;
+          color: #404040;
+          padding-left: 5vw;
+          line-height: 5vw;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          overflow: hidden;
+          &::after {
+            content: '';
+            display: block;
+            width: 4vw;
+            height: 4vw;
+            position: absolute;
+            left: 0;
+            top: 50%;
+            transform: translateY(-50%);
+            background-size: 4vw 4vw;
+            background-repeat: no-repeat;
+          }
+        }
+        .mobile {
+          &::after {
+            background-image: url(~assets/theme-images/common/h5-icon-mobile.png);
+          }
+        }
+        .address {
+          &::after {
+            background-image: url(~assets/theme-images/common/h5-icon-address.png);
+          }
+        }
+
+        .distance {
+          position: absolute;
+          font-size: 3vw;
+          color: #404040;
+          top: 0.8vw;
+          right: 0;
+        }
+      }
+    }
+  }
+}
+</style>

+ 703 - 0
pages/_template/hyt/approve/device/detail.vue

@@ -0,0 +1,703 @@
+<template>
+  <div class="device-detail" v-if="productInfo">
+    <div class="detail-title">设备认证</div>
+    <div class="page-top">
+      <div class="swiper-body">
+        <img :src="productInfo.pcImage" class="device-image" />
+        <div class="auth-seal"></div>
+        <img
+          class="auth-card"
+          :src="authCardImage"
+          @click="onShowAuthCard"
+          v-if="!showAuthCard"
+        />
+        <img class="auth-logo" :src="brandLogoImage" />
+      </div>
+      <div class="device-info">
+        <div class="logo">
+          <div class="logo-swiper" v-if="clubLogo.length > 1">
+            <SimpleSwiper
+              :imageList="clubLogo"
+              :pagination="false"
+            ></SimpleSwiper>
+          </div>
+          <img :src="clubLogo[0]" alt="logo" v-else />
+        </div>
+        <div class="section">
+          <div class="name" v-text="productInfo.productName"></div>
+          <div class="sncode mobile">
+            SN码:<span>{{ productInfo.snCode | snCodeRender }}</span>
+          </div>
+          <div class="row">
+            <!-- <span>产地:{{ productInfo.producePlace }}</span> -->
+            <span>品牌:{{ productInfo.brandName }}</span
+            ><i></i><span>产地:{{ productInfo.producePlace }}</span>
+            <!-- <span>品牌:RÖS'S</span><i></i><span>产地:西班牙巴塞罗那</span> -->
+          </div>
+          <div class="sncode pc">
+            SN码:<span>{{ productInfo.snCode | snCodeRender }}</span>
+          </div>
+          <div class="maker">制造商:{{ productInfo.manufacturer }}</div>
+          <div class="supplier">供应商:{{ productInfo.shopName }}</div>
+          <!-- <div class="supplier">供应商:ACEBELLE生物科技(深圳)有限公司</div> -->
+        </div>
+        <div class="auth" v-if="productInfo">
+          <div class="auth-icon"></div>
+          <template v-for="item in productInfo.clubList">
+            <div class="auth-info" :key="item.authId">
+              <span>该设备由</span>
+              <span class="font-bold">ROS'S</span>
+              <span>官方授权</span>
+              <span>{{ item.authParty }}</span>
+            </div>
+          </template>
+        </div>
+      </div>
+    </div>
+    <div class="page-content">
+      <div class="device-params">
+        <div class="title">相关参数</div>
+        <div class="line"></div>
+        <div class="params">
+          <div class="row" v-for="(row, index) in paramListRender" :key="index">
+            <template v-for="(col, index) in row">
+              <div class="col" :key="'name' + index">
+                {{ col.paramName }}{{ isPc ? ':' : '' }}
+              </div>
+              <div class="col" :key="'content' + index">
+                {{ col.paramContent }}
+              </div>
+            </template>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 授权牌弹窗 -->
+    <div class="mask" v-if="showAuthCard" @click="onHideAuthCard"></div>
+    <transition
+      enter-active-class="animate__zoomIn"
+      leave-active-class="animate__zoomOut"
+    >
+      <div class="auth-card-content animate__animated" v-if="showAuthCard">
+        <div class="auth-card-popup">
+          <span class="el-icon-circle-close" @click="onHideAuthCard"></span>
+          <img :src="authCardImage" />
+        </div>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script>
+import deviceDetailMixin from '@/mixins/deviceDetail'
+export default {
+  layout: 'app-hyt',
+  mixins: [deviceDetailMixin],
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .swiper-pagination-bullet {
+    background: #4093B5 !important;
+  }
+
+  .simple-swiper {
+    .swiper-pagination-bullet-active {
+      background: #4093B5;
+    }
+  }
+}
+
+.mask {
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  left: 0;
+  top: 0;
+  z-index: 8;
+  background: rgba(0, 0, 0, 0.5);
+}
+
+// pc 端
+@media screen and (min-width: 768px) {
+  .device-detail {
+    margin-bottom: 48px;
+
+    .auth-card-content {
+      width: 100vw;
+      height: 100vh;
+      position: fixed;
+      left: 0;
+      top: 0;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      z-index: 9;
+      .auth-card-popup {
+        position: relative;
+        width: 622px;
+
+        img {
+          display: block;
+          width: 100%;
+          height: auto;
+        }
+
+        .el-icon-circle-close {
+          position: absolute;
+          top: -50px;
+          right: 0;
+          font-size: 32px;
+          color: #fff;
+          cursor: pointer;
+        }
+      }
+    }
+
+    .detail-title {
+      font-size: 24px;
+      color: #282828;
+      margin: 32px auto 24px;
+      width: 1200px;
+    }
+
+    .page-top,
+    .page-content {
+      width: 1200px;
+      margin: 0 auto;
+      background: #fff;
+    }
+    .page-top {
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-start;
+      padding: 24px;
+      padding-right: 40px;
+      .swiper-body {
+        position: relative;
+        width: 540px;
+        height: 540px;
+        background: #f7f7f7;
+        .device-image {
+          width: 100%;
+          height: 100%;
+        }
+
+        .auth-seal {
+          position: absolute;
+          width: 70px;
+          height: 70px;
+          background: url(~assets/theme-images/hyt/pc/icon-auth-seal.png)
+            no-repeat center;
+          background-size: 70px;
+          right: 24px;
+          bottom: 24px;
+          z-index: 2;
+        }
+
+        .auth-card {
+          position: absolute;
+          width: auto;
+          height: 110px;
+          display: block;
+          bottom: 24px;
+          left: 24px;
+          z-index: 2;
+          cursor: pointer;
+        }
+
+        .auth-logo {
+          position: absolute;
+          max-width: 120px;
+          max-height: 120px;
+          top: 24px;
+          left: 24px;
+          z-index: 2;
+        }
+      }
+      .device-info {
+        width: 572px;
+        position: relative;
+
+        .section {
+          width: 440px;
+          word-break: break-all;
+        }
+
+        .logo {
+          width: 114px;
+          height: 114px;
+          border-radius: 50%;
+          // background: #d8d8d8;
+          border: 1px solid #d8d8d8;
+          // box-sizing: border-box;
+          position: absolute;
+          right: 0;
+          top: 0;
+
+          .logo-swiper {
+            width: 112px;
+            height: 112px;
+            overflow: hidden;
+            border-radius: 50%;
+          }
+
+          ::v-deep {
+            img {
+              width: 112px;
+              height: 112px;
+              border-radius: 50%;
+            }
+          }
+
+          &::after {
+            content: '';
+            position: absolute;
+            z-index: 1;
+            right: 6px;
+            bottom: 0;
+            display: block;
+            width: 24px;
+            height: 24px;
+            background: url(~assets/theme-images/hyt/pc/icon-auth-ren.png)
+              no-repeat center;
+            background-size: 23px;
+          }
+
+          img {
+            display: block;
+            width: 100%;
+            height: 100%;
+            // background: pink;
+            border-radius: 50%;
+          }
+        }
+
+        .name {
+          font-size: 24px;
+          color: #282828;
+          line-height: 1.6;
+          margin-bottom: 24px;
+          font-weight: bold;
+        }
+
+        .row,
+        .sncode,
+        .maker {
+          margin-bottom: 16px;
+        }
+        .row {
+          line-height: 24px;
+          i {
+            position: relative;
+            margin: 0 16px;
+            &::after {
+              content: '';
+              display: block;
+              width: 1px;
+              height: 16px;
+              background: #999999;
+              position: absolute;
+              top: 0;
+              left: 0;
+            }
+          }
+
+          span {
+            font-size: 18px;
+            color: #999999;
+          }
+        }
+
+        .sncode.mobile {
+          display: none;
+        }
+
+        .sncode.pc {
+          font-size: 18px;
+          color: #282828;
+        }
+        .supplier,
+        .maker {
+          font-size: 20px;
+          color: #282828;
+        }
+
+        .auth {
+          width: 100%;
+          min-height: 114px;
+          background: #4093B5;
+          margin-top: 56px;
+          box-sizing: border-box;
+          padding: 24px;
+
+          .auth-icon {
+            height: 28px;
+            background: url(~assets/theme-images/hyt/pc/icon-auth2.png)
+              no-repeat left center;
+            background-size: auto 28px;
+            margin-bottom: 10px;
+          }
+
+          .auth-info {
+            font-size: 0;
+            span {
+              font-size: 20px;
+              color: #fff;
+            }
+          }
+        }
+      }
+    }
+    .page-content {
+      margin-top: 16px;
+      box-sizing: border-box;
+      padding: 24px;
+      .device-params {
+        .title {
+          font-size: 28px;
+          color: #282828;
+          font-weight: bold;
+        }
+        .line {
+          height: 1px;
+          background: #ececec;
+          position: relative;
+          margin-top: 10px;
+          margin-bottom: 20px;
+          &::after {
+            content: '';
+            position: absolute;
+            width: 73px;
+            height: 2px;
+            background: #4093B5;
+            left: 0;
+            bottom: 0;
+          }
+        }
+        .params {
+          width: 100%;
+          .row {
+            display: table-row;
+            width: 100%;
+          }
+
+          .col {
+            display: table-cell;
+            font-size: 16px;
+            padding: 12px 0;
+
+            &:nth-child(2n-1) {
+              color: #999999;
+              white-space: nowrap;
+            }
+
+            &:nth-child(2n) {
+              color: #282828;
+              padding-left: 12px;
+            }
+
+            &:nth-child(3) {
+              padding-left: 100px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动端
+@media screen and (max-width: 768px) {
+  .device-detail {
+    .detail-title {
+      display: none;
+    }
+
+    .auth-card-content {
+      width: 100vw;
+      height: 100vh;
+      position: fixed;
+      left: 0;
+      top: 0;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      z-index: 9;
+      .auth-card-popup {
+        position: relative;
+        width: 86vw;
+
+        img {
+          display: block;
+          width: 100%;
+          height: auto;
+        }
+
+        .el-icon-circle-close {
+          position: absolute;
+          top: -8vw;
+          right: 0;
+          font-size: 7vw;
+          color: #fff;
+          cursor: pointer;
+        }
+      }
+    }
+
+    .page-top,
+    .page-content {
+      margin: 0 auto;
+      background: #fff;
+    }
+    .page-top {
+      .swiper-body {
+        position: relative;
+        width: 100vw;
+        height: 100vw;
+        background: #f7f7f7;
+
+        .device-image {
+          width: 100%;
+          height: 100%;
+        }
+
+        ::v-deep {
+          img {
+            width: 100vw;
+            height: 100vw;
+          }
+        }
+
+        .auth-seal {
+          position: absolute;
+          width: 13.8vw;
+          height: 13.8vw;
+          background: url(~assets/theme-images/hyt/h5/icon-auth-seal.png)
+            no-repeat center;
+          background-size: 13.8vw;
+          right: 4vw;
+          bottom: 4vw;
+          z-index: 2;
+        }
+
+        .auth-card {
+          position: absolute;
+          width: auto;
+          height: 20.6vw;
+          display: block;
+          bottom: 4vw;
+          left: 4vw;
+          z-index: 2;
+        }
+
+        .auth-logo {
+          position: absolute;
+          max-width: 18vw;
+          max-height: 18vw;
+          top: 4vw;
+          left: 4vw;
+          z-index: 2;
+        }
+      }
+      .device-info {
+        position: relative;
+
+        .section {
+          word-break: break-all;
+          padding: 4vw 4vw 0;
+        }
+
+        .logo {
+          width: 18vw;
+          height: 18vw;
+          border-radius: 50%;
+          // background: #d8d8d8;
+          border: 0.1vw solid #d8d8d8;
+          box-sizing: border-box;
+          position: absolute;
+          right: 4vw;
+          top: 5.8vw;
+
+          .logo-swiper {
+            width: 18vw;
+            height: 18vw;
+            overflow: hidden;
+            border-radius: 50%;
+          }
+
+          ::v-deep {
+            img {
+              width: 18vw;
+              height: 18vw;
+              border-radius: 50%;
+            }
+          }
+
+          &::after {
+            content: '';
+            position: absolute;
+            z-index: 1;
+            right: 0.7vw;
+            bottom: 0;
+            display: block;
+            width: 3.6vw;
+            height: 3.6vw;
+            background: url(~assets/theme-images/hyt/h5/icon-auth-ren.png)
+              no-repeat center;
+            background-size: 3.6vw;
+          }
+
+          img {
+            display: block;
+            width: 100%;
+            height: 100%;
+            // background: pink;
+            border-radius: 50%;
+          }
+        }
+
+        .name {
+          font-size: 5.4vw;
+          color: #282828;
+          line-height: 7.4vw;
+          margin-bottom: 2.4vw;
+          font-weight: bold;
+          max-width: 70vw;
+        }
+
+        .sncode.pc {
+          display: none;
+        }
+        .sncode.mobile {
+          margin: 2.4vw 0 5.6vw;
+          color: #282828;
+          font-size: 4vw;
+        }
+
+        .row {
+          line-height: 4.7vw;
+          i {
+            position: relative;
+            margin: 0 4vw;
+            &::after {
+              content: '';
+              display: block;
+              width: 0.2vw;
+              height: 3vw;
+              background: #282828;
+              position: absolute;
+              top: 1vw;
+              left: 0;
+            }
+          }
+
+          span {
+            font-size: 3.6vw;
+            color: #282828;
+          }
+        }
+
+        .row,
+        .maker {
+          margin-bottom: 1.2vw;
+        }
+
+        .supplier,
+        .maker {
+          font-size: 3.6vw;
+          color: #282828;
+        }
+
+        .auth {
+          // width: 100%;
+          margin: 0 4vw;
+          min-height: 20vw;
+          background: #4093B5;
+          margin-top: 4vw;
+          box-sizing: border-box;
+          padding: 5.2vw 4vw;
+          border-radius: 1.2vw;
+
+          .auth-icon {
+            height: 4.9vw;
+            background: url(~assets/theme-images/hyt/h5/icon-auth2.png)
+              no-repeat left center;
+            background-size: auto 4.9vw;
+            margin-bottom: 1vw;
+          }
+
+          .auth-info {
+            font-size: 0;
+            span {
+              font-size: 3.6vw;
+              line-height: 6.4vw;
+              color: #fff;
+              font-weight: bold;
+            }
+          }
+        }
+      }
+    }
+    .page-content {
+      .device-params {
+        padding: 8vw 4vw;
+        box-sizing: border-box;
+        .title {
+          font-size: 4.6vw;
+          color: #282828;
+          font-weight: bold;
+        }
+        .line {
+          // height: 0.2vw;
+          // background: #ececec;
+          position: relative;
+          margin-top: 4.7vw;
+          margin-bottom: 1.2vw;
+        }
+        .params {
+          width: 100%;
+          word-break: break-all;
+          text-align: justify;
+          .row {
+            display: table-row;
+            width: 100%;
+
+            &:first-child {
+              .col {
+                padding-top: 0;
+              }
+            }
+
+            &:last-child {
+              .col {
+                padding-bottom: 0;
+              }
+            }
+          }
+
+          .col {
+            display: table-cell;
+            font-size: 3.6vw;
+            padding: 1.6vw 0;
+
+            &:nth-child(2n-1) {
+              color: #282828;
+              padding-right: 3.2vw;
+              font-weight: bold;
+              white-space: nowrap;
+              border-right: 0.2vw solid #d8d8d8;
+            }
+
+            &:nth-child(2n) {
+              color: #4e4e4e;
+              padding-left: 3.2vw;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 377 - 0
pages/_template/hyt/approve/device/index.vue

@@ -0,0 +1,377 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="loadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top"></div>
+      <div class="page-content">
+        <div class="search-title">设备种类查询:</div>
+        <div class="search-container">
+          <el-select
+            v-model="listQuery.productTypeId"
+            slot="prepend"
+            placeholder="设备种类"
+            class="select-type"
+          >
+            <template v-for="product in productSelectList">
+              <el-option
+                :label="product.name"
+                :value="product.productTypeId"
+                :key="product.productTypeId"
+              ></el-option>
+            </template>
+          </el-select>
+          <el-input
+            :placeholder="
+              searchQuery.type === 1 ? '请输入SN码' : '请输入完整的机构名称'
+            "
+            v-model="searchQuery.keyword"
+            class="input-with-select"
+          >
+            <el-select
+              v-model="searchQuery.type"
+              slot="prepend"
+              placeholder="请选择"
+            >
+              <el-option label="SN码" :value="1"></el-option>
+              <el-option label="机构名" :value="2"></el-option>
+            </el-select>
+            <i slot="prefix" class="el-input__icon el-icon-search"></i>
+          </el-input>
+          <el-button class="submit" type="warning" @click="onSearch"
+            >查询</el-button
+          >
+        </div>
+        <template v-if="list.length > 0 || searchFlag">
+          <!-- 标题 -->
+          <div class="title px-4 pt-12 pb-6 md:px-0">
+            共<span v-text="total"></span>台设备
+          </div>
+          <!-- 列表 -->
+          <div class="list">
+            <div
+              class="section flex justify-between mb-4"
+              v-for="item in list"
+              :key="item.productId"
+              @click="toDetail(item)"
+            >
+              <img class="cover" :src="item.productImage" />
+              <div class="info">
+                <div class="name" v-text="item.productName"></div>
+                <div class="code">SN码:{{ item.snCode | formatSnCode }}</div>
+                <el-popover placement="right" title="机构列表" trigger="hover">
+                  <template v-for="club in item.clubList">
+                    <div
+                      @click.stop="toClubDetail(club)"
+                      :key="club.authId"
+                      class="club-item"
+                    >
+                      {{ club.authParty }}
+                    </div>
+                  </template>
+                  <div class="club-name" slot="reference">
+                    所属机构:
+                    <template v-for="(club, index) in item.clubList">
+                      <span :key="club.authId"
+                        ><i v-if="index !== 0">,</i>{{ club.authParty }}</span
+                      >
+                    </template>
+                  </div>
+                </el-popover>
+              </div>
+            </div>
+          </div>
+          <!-- 列表为空 -->
+          <SimpleEmpty
+            v-if="!total && !isRequest"
+            name="icon-empty-device.png"
+            description="未找到相关设备~"
+          ></SimpleEmpty>
+        </template>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import deviceListMixin from '@/mixins/deviceList'
+export default {
+  layout: 'app-hyt',
+  mixins: [deviceListMixin],
+}
+</script>
+
+<style scoped lang="scss">
+.input-with-select {
+  ::v-deep {
+    .el-input-group__prepend {
+      background-color: #fff;
+    }
+  }
+}
+
+::v-deep {
+  .el-button--warning {
+    background-color: #4093B5;
+    border-color: #4093B5;
+
+    &:hover {
+      background: #3483a1;
+      border-color: #3483a1;
+    }
+  }
+}
+
+// pc 端
+@media screen and (min-width: 768px) {
+  .el-popover {
+    .club-item {
+      line-height: 24px;
+      cursor: pointer;
+      &:hover {
+        color: #4093B5;
+      }
+    }
+  }
+
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+  }
+  .page-top {
+    height: 420px;
+    background: url(~assets/theme-images/hyt/pc/banner-device.png);
+    background-size: auto 420px;
+    background-position: center;
+
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1000px;
+    margin: 0 auto;
+
+    .search-title {
+      font-size: 16px;
+      color: #404040;
+      margin: 25px 0;
+    }
+
+    .search-container {
+      display: flex;
+      justify-content: space-between;
+      .input-with-select {
+        margin: 0 15px;
+        .el-select {
+          ::v-deep {
+            .el-input {
+              width: 130px;
+            }
+          }
+        }
+      }
+
+      .submit {
+        width: 295px;
+      }
+    }
+
+    .title {
+      font-size: 16px;
+      color: #404040;
+
+      span {
+        color: #4093B5;
+      }
+    }
+
+    .list {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      justify-content: space-between;
+
+      .empty {
+        width: 390px;
+      }
+
+      .section {
+        width: 490px;
+        height: 136px;
+        background-color: #f3f5f6;
+        border-radius: 4px;
+        box-sizing: border-box;
+        padding: 16px;
+        cursor: pointer;
+        transition: all 0.4s;
+        &:hover {
+          box-shadow: 0 0 24px rgba(0, 0, 0, 0.2);
+        }
+
+        .cover {
+          display: block;
+          width: 104px;
+          height: 104px;
+        }
+        .info {
+          width: 336px;
+          position: relative;
+
+          .name {
+            font-size: 18px;
+            color: #101010;
+            font-weight: bold;
+            margin-bottom: 16px;
+            margin-top: 4px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+          }
+          .code,
+          .club-name {
+            position: relative;
+            font-size: 14px;
+            color: #666;
+            line-height: 24px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+            margin-top: 6px;
+
+            span {
+              color: #1890ff;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    background: url(~assets/theme-images/hyt/pc/banner-device.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+  .page-content {
+    position: relative;
+
+    .search-title {
+      font-size: 3.4vw;
+      color: #404040;
+      margin: 3.2vw;
+    }
+
+    .search-container {
+      padding: 0 3.2vw;
+
+      .select-type {
+        width: 100%;
+      }
+
+      .input-with-select {
+        margin: 3.2vw 0;
+        .el-select {
+          ::v-deep {
+            .el-input {
+              width: 130px;
+            }
+          }
+        }
+      }
+
+      .submit {
+        width: 100%;
+      }
+    }
+
+    .title {
+      font-size: 3.4vw;
+      color: #404040;
+
+      span {
+        color: #4093B5;
+      }
+    }
+
+    .list {
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+
+      .section {
+        width: 93.6vw;
+        height: 26vw;
+        background-color: #f3f5f6;
+        border-radius: 4px;
+        box-sizing: border-box;
+        padding: 3.2vw;
+
+        .cover {
+          display: block;
+          width: 19.6vw;
+          height: 19.6vw;
+        }
+        .info {
+          width: 64vw;
+          position: relative;
+          margin-left: 3.2vw;
+          .name {
+            font-size: 4vw;
+            color: #101010;
+            font-weight: bold;
+            margin-bottom: 4vw;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+          }
+          .code,
+          .club-name {
+            width: 66vw;
+            position: relative;
+            font-size: 3vw;
+            color: #404040;
+            line-height: 5vw;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+
+            span {
+              color: #6d9eff;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 300 - 0
pages/_template/hyt/approve/personnel/operate/detail.vue

@@ -0,0 +1,300 @@
+<template>
+  <div class="page md:flex md:justify-between">
+    <div class="page-title">体疗师认证</div>
+    <div class="page-top">
+      <div class="swiper">
+        <SimpleSwiper :imageList="doctorInfo.bannerList"></SimpleSwiper>
+      </div>
+    </div>
+    <div class="page-content">
+      <div class="doctor-info px-4 pt-4 md:pt-0">
+        <div class="name pb-4">{{ doctorInfo.doctorName }}</div>
+        <div class="tag pb-1">{{ doctorInfo.tagList.join(' | ') }}</div>
+        <div class="code pb-1">
+          从业资格证编号:{{ doctorInfo.certificateNo }}
+        </div>
+        <div class="club-name">所在机构:{{ doctorInfo.clubName }}</div>
+      </div>
+      <div class="section param-list pb-4">
+        <div
+          class="param px-4 pt-4"
+          v-for="(param, index) in doctorInfo.paramList"
+          :key="index"
+        >
+          <div class="name py-2" v-text="param.name"></div>
+          <div class="content" v-text="param.content"></div>
+        </div>
+      </div>
+      <div class="divider"></div>
+      <div class="device-list p-4">
+        <div class="title">具备操作资格设备</div>
+        <div class="list">
+          <div
+            class="device flex items-center py-4"
+            v-for="item in doctorInfo.equipmentList"
+            :key="item.productId"
+          >
+            <img class="cover" :src="item.image" />
+            <div class="info">
+              <div class="name" v-text="item.equipmentName"></div>
+              <div class="brand mt-2">品牌:{{ item.brand }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import operatDoctorDetailMixin from '@/mixins/operatDoctorDetail'
+export default {
+  layout: 'app-hyt',
+  mixins: [operatDoctorDetailMixin],
+}
+</script>
+
+<style scoped lang="scss">
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    width: 1200px;
+    height: 612px;
+    margin-left: auto;
+    margin-right: auto;
+    margin-top: 80px;
+    background-color: #fff;
+    box-sizing: border-box;
+    padding: 16px;
+    padding-right: 0;
+  }
+
+  .page-title {
+    position: absolute;
+    font-size: 24px;
+    color: #333;
+    top: -50px;
+    left: 0;
+  }
+
+  .page-top {
+    .swiper {
+      width: 580px;
+      height: 580px;
+
+      background: #f7f7f7;
+      ::v-deep {
+        img {
+          width: 580px;
+          height: 580px;
+        }
+      }
+    }
+  }
+  .page-content {
+    width: 580px;
+    padding: 0 24px;
+    overflow-y: auto;
+    .doctor-info {
+      line-height: 1.6;
+      .name {
+        font-size: 24px;
+        color: #101010;
+        font-weight: bold;
+
+        &::after {
+          content: '';
+          display: inline-block;
+          width: 76px;
+          height: 28px;
+          background: url(~assets/theme-images/common/h5-icon-doctor-ad.png)
+            no-repeat;
+          background-size: 76px 28px;
+          vertical-align: -5px;
+          margin-left: 8px;
+        }
+      }
+      .tag {
+        font-size: 14px;
+        color: #909399;
+      }
+      .code,
+      .club-name {
+        font-size: 14px;
+        color: #404040;
+      }
+    }
+
+    .param-list {
+      .param {
+        .name {
+          font-size: 18px;
+          color: #101010;
+          font-weight: bold;
+        }
+        .content {
+          font-size: 14px;
+          color: #404040;
+          line-height: 1.6;
+          text-align: justify;
+        }
+      }
+    }
+
+    .device-list {
+      .title {
+        padding: 16px;
+        font-size: 20px;
+        font-weight: bold;
+        color: #404040;
+        background-color: #f3f5f6;
+      }
+      .list {
+        display: flex;
+        align-items: center;
+        flex-direction: column;
+        .device {
+          width: 100%;
+          box-sizing: border-box;
+          border-bottom: 1px solid #d8d8d8;
+          .cover {
+            width: 84px;
+            height: 84px;
+            display: block;
+          }
+          .info {
+            margin-left: 16px;
+            width: 300px;
+            .name {
+              font-size: 18px;
+              color: #101010;
+              text-overflow: ellipsis;
+              overflow: hidden;
+              white-space: nowrap;
+            }
+            .brand {
+              font-size: 14px;
+              color: #666666;
+              text-overflow: ellipsis;
+              overflow: hidden;
+              white-space: nowrap;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-title {
+    display: none;
+  }
+  .page-top {
+    .swiper {
+      height: 100vw;
+
+      background: #f7f7f7;
+      ::v-deep {
+        img {
+          height: 100vw;
+        }
+      }
+    }
+  }
+  .page-content {
+    .divider {
+      height: 3.2vw;
+      background-color: #f7f7f7;
+    }
+    .doctor-info {
+      line-height: 1.6;
+      .name {
+        font-size: 5vw;
+        color: #101010;
+        font-weight: bold;
+
+        &::after {
+          content: '';
+          display: inline-block;
+          width: 13.6vw;
+          height: 5vw;
+          background: url(~assets/theme-images/common/h5-icon-doctor-ad.png)
+            no-repeat;
+          background-size: 13.6vw 5vw;
+          vertical-align: -0.8vw;
+          margin-left: 1.2vw;
+        }
+      }
+      .tag {
+        font-size: 3vw;
+        color: #909399;
+      }
+      .code,
+      .club-name {
+        font-size: 3.2vw;
+        color: #404040;
+      }
+    }
+
+    .param-list {
+      .param {
+        .name {
+          font-size: 3.8vw;
+          color: #101010;
+          font-weight: bold;
+        }
+        .content {
+          font-size: 3.2vw;
+          color: #404040;
+          line-height: 1.6;
+          text-align: justify;
+        }
+      }
+    }
+
+    .device-list {
+      .title {
+        font-size: 4vw;
+        color: #101010;
+        font-weight: bold;
+      }
+      .list {
+        display: flex;
+        align-items: center;
+        flex-direction: column;
+        .device {
+          width: 100%;
+          box-sizing: border-box;
+          border-bottom: 0.1vw solid #d8d8d8;
+          .cover {
+            width: 19.6vw;
+            height: 19.6vw;
+            display: block;
+          }
+          .info {
+            margin-left: 3.2vw;
+            width: 66vw;
+            .name {
+              font-size: 3.6vw;
+              color: #101010;
+              text-overflow: ellipsis;
+              overflow: hidden;
+              white-space: nowrap;
+            }
+            .brand {
+              font-size: 3vw;
+              color: #666666;
+              text-overflow: ellipsis;
+              overflow: hidden;
+              white-space: nowrap;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 282 - 0
pages/_template/hyt/approve/personnel/operate/index.vue

@@ -0,0 +1,282 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="loadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top"></div>
+      <div class="page-content">
+        <!-- 搜索区域 -->
+        <div class="search">
+          <SimpleSearch
+            v-model="listQuery.doctorName"
+            @search="onSearch"
+            placeholder="搜索体疗师"
+          />
+        </div>
+        <!-- 标题 -->
+        <div class="title px-4 pt-12 pb-6">
+          共<span v-text="total"></span>位体疗师
+        </div>
+        <!-- 列表 -->
+        <div class="list">
+          <div
+            class="section flex justify-between mb-4"
+            v-for="item in list"
+            :key="item.doctorId"
+            @click="toDetail(item)"
+          >
+            <img class="cover" :src="item.doctorImage" />
+            <div class="info">
+              <div class="name" v-text="item.doctorName"></div>
+              <div class="tag">{{ item.tagList.join(' | ') }}</div>
+              <div class="code">
+                资格证编号:<span>{{ item.certificateNo }}</span>
+              </div>
+              <div class="club-name">
+                所在机构:<span>{{ item.clubName }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          name="icon-empty-doctor.png"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import operatDoctorListMixin from '@/mixins/operatDoctorList'
+export default {
+  layout: 'app-hyt',
+  mixins: [operatDoctorListMixin],
+}
+</script>
+
+<style scoped lang="scss">
+.page {
+  position: relative;
+  min-height: calc(100vh - 80px - 80px);
+  background-color: #fff;
+}
+// pc 端
+@media screen and (min-width: 768px) {
+  .page-top {
+    height: 420px;
+    background: url(~assets/theme-images/hyt/pc/banner-doctor.png) no-repeat
+      center;
+    background-size: auto 420px;
+
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+
+    .logo,
+    .name {
+      transform: translateY(-30px);
+    }
+  }
+  .page-content {
+    width: 1000px;
+    margin: 0 auto;
+
+    .search {
+      position: absolute;
+      left: 50%;
+      top: 300px;
+      transform: translateX(-50%);
+    }
+
+    .title {
+      font-size: 16px;
+      color: #404040;
+
+      span {
+        color: #4093B5;
+      }
+    }
+
+    .list {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      justify-content: space-between;
+
+      .empty {
+        width: 390px;
+      }
+
+      .section {
+        width: 490px;
+        height: 136px;
+        background-color: #f3f5f6;
+        border-radius: 4px;
+        box-sizing: border-box;
+        padding: 16px;
+
+        cursor: pointer;
+        transition: all 0.4s;
+        &:hover {
+          box-shadow: 0 0 24px rgba(0, 0, 0, 0.2);
+        }
+
+        .cover {
+          display: block;
+          width: 104px;
+          height: 104px;
+        }
+        .info {
+          width: 334px;
+          position: relative;
+          margin-left: 12px;
+          .name {
+            font-size: 18px;
+            color: #101010;
+            font-weight: bold;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+            font-weight: bold;
+            margin-bottom: 8px;
+          }
+          .tag,
+          .code,
+          .club-name {
+            height: 20px;
+            position: relative;
+            font-size: 14px;
+            color: #999;
+            line-height: 20px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+            margin-top: 4px;
+
+            span {
+              color: #333333;
+            }
+          }
+
+          .tag {
+            color: #909399;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    background: url(~assets/theme-images/hyt/pc/banner-doctor.png) no-repeat
+      center;
+    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 {
+    position: relative;
+    .search {
+      position: absolute;
+      left: 50%;
+      top: 0;
+      transform: translate(-50%, -50%);
+    }
+
+    .city {
+      padding-top: 12vw;
+    }
+
+    .title {
+      font-size: 3.4vw;
+      color: #404040;
+
+      span {
+        color: #4093B5;
+      }
+    }
+
+    .list {
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+
+      .section {
+        width: 93.6vw;
+        background-color: #f3f5f6;
+        border-radius: 4px;
+        box-sizing: border-box;
+        padding: 3.2vw;
+
+        .cover {
+          display: block;
+          width: 21.6vw;
+          height: 21.6vw;
+        }
+        .info {
+          position: relative;
+          margin-left: 3.2vw;
+          .name {
+            font-size: 4vw;
+            color: #101010;
+            font-weight: bold;
+            margin-bottom: 0.8vw;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+          }
+          .tag,
+          .code,
+          .club-name {
+            width: 62vw;
+            height: 5vw;
+            position: relative;
+            font-size: 3vw;
+            color: #999;
+            line-height: 5vw;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+
+            span {
+              color: #333;
+            }
+          }
+          .tag {
+            color: #909399;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 468 - 0
pages/_template/hyt/center/club-detail.vue

@@ -0,0 +1,468 @@
+<template>
+  <div class="club-detail page">
+    <div class="page-top"></div>
+    <div class="page-content">
+      <div class="title">机构认证信息</div>
+      <template v-if="!isAuth">
+        <div class="tip">抱歉,您暂未认证机构</div>
+        <div class="btn" @click="toAuth">去认证</div>
+      </template>
+      <template v-else>
+        <div class="row">
+          <div class="col label">机构名称:</div>
+          <div class="col content">{{ clubInfo.authParty }}</div>
+        </div>
+        <div class="row">
+          <div class="col label">联系电话:</div>
+          <div class="col content">{{ clubInfo.mobile }}</div>
+        </div>
+        <div class="row">
+          <div class="col label">运营联系人:</div>
+          <div class="col content">{{ clubInfo.linkMan }}</div>
+        </div>
+        <div class="row">
+          <div class="col label">运营联系人手机号:</div>
+          <div class="col content">{{ clubInfo.linkMobile }}</div>
+        </div>
+        <div class="row">
+          <div class="col label">所在地区:</div>
+          <div class="col content">{{ clubInfo.area }}</div>
+        </div>
+        <div class="row">
+          <div class="col label">详细地址:</div>
+          <div class="col content">{{ clubInfo.address }}</div>
+        </div>
+        <div class="row">
+          <div class="col label">所在位置:</div>
+          <div class="col content">
+            <div class="postion-btn" @click="initMap">查看定位</div>
+          </div>
+        </div>
+        <div class="row block">
+          <div class="col label">logo:</div>
+          <div class="col content">
+            <el-image :src="clubInfo.logo" v-if="clubInfo.logo"></el-image>
+          </div>
+        </div>
+        <div class="row block">
+          <div class="col label">门头照:</div>
+          <div class="col content">
+            <template v-for="(image, index) in clubInfo.bannerList">
+              <el-image :src="image" :key="index"></el-image>
+            </template>
+          </div>
+        </div>
+        <div class="row">
+          <div class="col label">机构类型:</div>
+          <div class="col content">{{ firstClubTypeName }}</div>
+        </div>
+        <div class="row">
+          <div class="col label">医美类型:</div>
+          <div class="col content">{{ secondClubTypeName }}</div>
+        </div>
+        <div class="row block">
+          <div class="col label">医疗许可证:</div>
+          <div class="col content">
+            <el-image
+              :src="clubInfo.medicalLicenseImage"
+              v-if="clubInfo.medicalLicenseImage"
+            ></el-image>
+          </div>
+        </div>
+        <div class="row">
+          <div class="col label">员工人数:</div>
+          <div class="col content">{{ clubInfo.empNum }}</div>
+        </div>
+      </template>
+    </div>
+
+    <div class="position-select" v-if="mapVisiable">
+      <div class="position-select-container">
+        <SimpleAMap ref="aMap" :lnglat="lnglat" />
+        <div class="position-select-footer">
+          <div class="lnglat">当前经纬度:{{ clubInfo.lngAndLat }}</div>
+          <div
+            class="position-confirm postion-control"
+            @click="mapVisiable = false"
+          >
+            确定
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-hyt',
+  data() {
+    return {
+      mapVisiable: false,
+      clubInfo: {},
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'routePrefix']),
+    isAuth() {
+      return this.clubInfo.auditStatus === 1
+    },
+    firstClubTypeName() {
+      if (!this.clubInfo.firstClubType) return '其他'
+      return ['医美', '生美', '项目公司', '个人', '其他'][
+        this.clubInfo.firstClubType - 1
+      ]
+    },
+    secondClubTypeName() {
+      if (!this.clubInfo.secondClubType) return '其他'
+      return ['诊所', '门诊', '医院', '其他', '美容院', '养生馆', '其他'][
+        this.clubInfo.secondClubType - 1
+      ]
+    },
+    lnglat() {
+      return this.clubInfo.lngAndLat ? this.clubInfo.lngAndLat.split(',') : null
+    },
+  },
+  created() {
+    this.fetchClubDetail()
+  },
+  methods: {
+    // 获取机构详情
+    async fetchClubDetail() {
+      try {
+        const authId = this.userInfo.authId
+        if (!authId) return
+        const res = await this.$http.api.fetchClubAuthInfoData({ authId })
+        this.clubInfo = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 去认证
+    toAuth() {
+      this.$router.push(`${this.routePrefix}/form/club-register`)
+    },
+    // 地图定位
+    initMap() {
+      this.mapVisiable = true
+      this.$nextTick(() => {
+        this.$refs.aMap.init()
+      })
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    display: flex;
+    justify-content: center;
+  }
+
+  .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;
+        }
+      }
+    }
+  }
+
+  .page-content {
+    max-width: 760px;
+    padding-bottom: 167px;
+    .title {
+      font-size: 24px;
+      color: #282828;
+      font-weight: bold;
+      text-align: center;
+      padding: 60px 0;
+    }
+
+    .tip {
+      font-size: 16px;
+      color: #b2b2b2;
+      margin-top: 200px;
+      margin-bottom: 24px;
+      text-align: center;
+    }
+
+    .btn {
+      width: 98px;
+      height: 36px;
+      background: #BC1724;
+      border-radius: 4px;
+      text-align: center;
+      line-height: 36px;
+      color: #fff;
+      font-size: 16px;
+      margin: 0 auto;
+      cursor: pointer;
+    }
+
+    .row {
+      display: flex;
+      align-items: flex-start;
+      margin: 32px 0;
+
+      .col {
+        font-size: 16px;
+      }
+      .label {
+        color: #666666;
+        min-width: 100px;
+      }
+      .content {
+        color: #282828;
+
+        .el-image {
+          width: 106px;
+          height: 106px;
+          border: 1px solid #dcdcdc;
+          margin-right: 4px;
+
+          &:last-child {
+            margin-right: 0;
+          }
+        }
+
+        .postion-btn {
+          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;
+          padding: 0 8px;
+
+          &::before {
+            content: '';
+            display: inline-block;
+            width: 16px;
+            height: 16px;
+            background: url(~assets/theme-images/common/icon-position.png)
+              no-repeat center;
+            background-size: 16px 16px;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    display: flex;
+    justify-content: center;
+  }
+
+  .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;
+        }
+      }
+    }
+  }
+
+  .page-content {
+    padding: 0 4vw;
+    .title {
+      font-size: 4.2vw;
+      color: #282828;
+      font-weight: bold;
+      text-align: center;
+      padding: 8vw 0;
+    }
+
+    .tip {
+      font-size: 3vw;
+      color: #b2b2b2;
+      margin-top: 60vw;
+      margin-bottom: 4.8vw;
+      text-align: center;
+    }
+
+    .btn {
+      width: 36vw;
+      height: 8.8vw;
+      background: #BC1724;
+      border-radius: 0.4vw;
+      text-align: center;
+      line-height: 8.8vw;
+      color: #fff;
+      font-size: 3.4vw;
+      margin: 0 auto;
+    }
+
+    .row {
+      display: flex;
+      align-items: flex-start;
+      margin: 5.6vw 0;
+
+      &.block {
+        display: block;
+        .label {
+          margin-bottom: 2.4vw;
+        }
+      }
+
+      .col {
+        font-size: 3.4vw;
+      }
+      .label {
+        color: #666666;
+        min-width: 20.4vw;
+      }
+      .content {
+        color: #282828;
+
+        .el-image {
+          width: 26vw;
+          height: 26vw;
+          border: 0.1vw solid #dcdcdc;
+          margin-right: 0.6vw;
+
+          &:last-child {
+            margin-right: 0;
+          }
+        }
+
+        .postion-btn {
+          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;
+          padding: 0 1.2vw;
+
+          &::before {
+            content: '';
+            display: inline-block;
+            width: 3.58vw;
+            height: 3.58vw;
+            background: url(~assets/theme-images/common/icon-position.png)
+              no-repeat center;
+            background-size: 3.58vw;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 252 - 0
pages/_template/hyt/center/device/detail.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="club-detail page">
+    <div class="page-top"></div>
+    <div class="page-content">
+      <div class="title">设备认证信息</div>
+      <div class="row">
+        <div class="col label">设备名称:</div>
+        <div class="col content">{{ productInfo.productName }}</div>
+      </div>
+      <div class="row block">
+        <div class="col label">设备图片:</div>
+        <div class="col content">
+          <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 label">所属品牌:</div>
+        <div class="col content">{{ productInfo.brandName }}</div>
+      </div>
+      <div class="row">
+        <div class="col label">购买渠道:</div>
+        <div class="col content">{{ productInfo.purchaseWay || '暂无' }}</div>
+      </div>
+      <div class="row block">
+        <div class="col label">发票:</div>
+        <div class="col content">
+          <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 label">设备SN码:</div>
+        <div class="col content">{{ productInfo.snCode }}</div>
+      </div>
+      <div class="row">
+        <div class="col label">设备参数:</div>
+        <div class="col content 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>
+</template>
+
+<script>
+export default {
+  layout: 'app-hyt',
+  data() {
+    return {
+      productInfo: {},
+    }
+  },
+  created() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      this.productId = this.$route.query.id
+      this.relationId = this.$route.query.relationId
+      this.getProductDetails()
+    },
+    // 获取认证机构信息
+    async getProductDetails() {
+      try {
+        const res = await this.$http.api.getProductDetails({
+          productId: this.productId,
+          relationId: this.relationId,
+        })
+        this.productInfo = { ...this.productInfo, ...res.data }
+        console.log('res', this.productInfo)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    display: flex;
+    justify-content: center;
+    background: #fff;
+    min-height: calc(100vh - 80px);
+  }
+
+  .page-content {
+    max-width: 760px;
+    padding-bottom: 167px;
+    .title {
+      font-size: 24px;
+      color: #282828;
+      font-weight: bold;
+      text-align: center;
+      padding: 60px 0;
+    }
+
+    .row {
+      display: flex;
+      align-items: flex-start;
+      margin: 32px 0;
+
+      .col {
+        font-size: 16px;
+      }
+      .label {
+        color: #666666;
+        min-width: 100px;
+      }
+      .content {
+        color: #282828;
+
+        .el-image {
+          width: 106px;
+          height: 106px;
+          border: 1px solid #dcdcdc;
+          margin-right: 4px;
+
+          &:last-child {
+            margin-right: 0;
+          }
+        }
+
+        &.params-list {
+          width: 100%;
+          margin-top: -10px;
+          .param {
+            display: table-row;
+            width: 100%;
+          }
+
+          .param-name,
+          .param-content {
+            display: table-cell;
+            font-size: 16px;
+            padding: 12px 0;
+
+            &:nth-child(2n-1) {
+              color: #999999;
+              white-space: nowrap;
+            }
+
+            &:nth-child(2n) {
+              color: #282828;
+              padding-left: 12px;
+            }
+
+            &:nth-child(3) {
+              padding-left: 100px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page-content {
+    padding: 0 7.2vw;
+    .title {
+      font-size: 4.6vw;
+      color: #282828;
+      font-weight: bold;
+      text-align: center;
+      padding: 8vw 0;
+    }
+
+    .row {
+      display: flex;
+      align-items: flex-start;
+      margin-bottom: 5.6vw;
+
+      &.block {
+        display: block;
+        .label {
+          margin-bottom: 2.4vw;
+        }
+      }
+
+      .col {
+        font-size: 3.4vw;
+      }
+      .label {
+        color: #666666;
+        min-width: 20.4vw;
+      }
+      .content {
+        color: #282828;
+
+        .el-image {
+          width: 26vw;
+          height: 26vw;
+          border: 0.1vw solid #dcdcdc;
+          margin-right: 0.6vw;
+
+          &:last-child {
+            margin-right: 0;
+          }
+        }
+
+        &.params-list {
+          width: 100%;
+          margin-top: -1vw;
+          .param {
+            display: table-row;
+            width: 100%;
+          }
+
+          .param-name,
+          .param-content {
+            display: table-cell;
+            font-size: 3.4vw;
+            padding: 1.2vw 0;
+
+            &:nth-child(2n-1) {
+              color: #999999;
+              white-space: nowrap;
+            }
+
+            &:nth-child(2n) {
+              color: #282828;
+              padding-left: 1.2vw;
+            }
+
+            &:nth-child(3) {
+              padding-left: 100px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 275 - 0
pages/_template/hyt/center/device/index.vue

@@ -0,0 +1,275 @@
+<template>
+  <div class="page">
+    <div class="page-content">
+      <div class="title">设备认证信息</div>
+      <template v-if="!isAuth">
+        <div class="tip">抱歉,您暂未认证设备</div>
+        <div class="btn" @click="toAuth">去认证</div>
+      </template>
+      <div class="device-list" v-else>
+        <div
+          class="device"
+          @click="toDetail(product)"
+          v-for="product in list"
+          :key="product.productId"
+        >
+          <div class="cover">
+            <img :src="product.image" :alt="product.productName" />
+          </div>
+          <div class="content">
+            <div class="name">{{ product.productName }}</div>
+            <div class="sncode">SN码:{{ product.snCode | formatSnCode }}</div>
+          </div>
+        </div>
+      </div>
+      <div class="to-auth-btn" v-if="isAuth && !isPc" @click="toAuth">
+        去认证
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-hyt',
+  data() {
+    return {
+      list: [],
+      clubInfo: {},
+    }
+  },
+  filters: {
+    formatSnCode(code) {
+      if (!code) return ''
+      return code.replace(/^(\w{2})\w+(\w{4})$/, '$1******$2')
+    },
+  },
+  computed: {
+    ...mapGetters(['routePrefix', 'userInfo', 'authUserId', 'isPc']),
+    isAuth() {
+      return this.clubInfo.auditStatus === 1 && this.list.length > 0
+    },
+  },
+  created() {
+    this.fetchProductList()
+    this.fetchAuthDetail()
+  },
+  methods: {
+    // 产看详情
+    toDetail(row) {
+      const path = `${this.routePrefix}/center/device/detail?productId=${row.productId}&relationId=${row.relationId}`
+      this.$router.push(path)
+    },
+    // 获取机构信息
+    async fetchAuthDetail() {
+      try {
+        if (this.userInfo && !this.userInfo.authId) return
+        const res = await this.$http.api.fetchClubAuthInfoData({
+          authId: this.userInfo.authId,
+        })
+        this.clubInfo = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 获取已认证设备列表
+    async fetchProductList() {
+      try {
+        const authId = this.userInfo.authId
+        if (!authId) return
+        const res = await this.$http.api.fetchClubAuthProductList({
+          authId,
+          authUserId: this.authUserId,
+        })
+        if (res.data) {
+          this.list = res.data
+        }
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 去认证
+    toAuth() {
+      this.$router.push(`${this.routePrefix}/form/club-register`)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@mixin ellipsis($line: 1) {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: $line;
+  -webkit-box-orient: vertical;
+}
+
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+    min-height: calc(100vh - 80px);
+  }
+
+  .page-content {
+    width: 1000px;
+    margin: 0 auto;
+    .title {
+      font-size: 24px;
+      text-align: center;
+      font-weight: bold;
+      padding: 60px 0;
+    }
+
+    .tip {
+      font-size: 16px;
+      color: #b2b2b2;
+      margin-top: 200px;
+      margin-bottom: 24px;
+      text-align: center;
+    }
+
+    .btn {
+      width: 98px;
+      height: 36px;
+      background: #bc1724;
+      border-radius: 4px;
+      text-align: center;
+      line-height: 36px;
+      color: #fff;
+      font-size: 16px;
+      margin: 0 auto;
+      cursor: pointer;
+    }
+
+    .device-list {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      flex-wrap: wrap;
+
+      .device {
+        width: 490px;
+        height: 136px;
+        background: #f3f5f6;
+        border-radius: 8px;
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        padding: 16px;
+        margin-bottom: 20px;
+        cursor: pointer;
+
+        .cover {
+          img {
+            display: block;
+            width: 106px;
+            height: 106px;
+          }
+        }
+        .content {
+          width: 320px;
+          margin-left: 16px;
+          .name {
+            font-size: 18px;
+            color: #282828;
+            font-weight: bold;
+            margin-bottom: 24px;
+            @include ellipsis(1);
+          }
+          .sncode {
+            font-size: 16px;
+            color: #666666;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page-content {
+    .title {
+      font-size: 4.2vw;
+      text-align: center;
+      font-weight: bold;
+      padding: 8vw 0;
+    }
+
+    .to-auth-btn {
+      width: 85.2vw;
+      height: 12vw;
+      background: #bc1724;
+      border-radius: 0.2vw;
+      text-align: center;
+      line-height: 12vw;
+      color: #ffffff;
+      font-size: 3.6vw;
+      position: fixed;
+      bottom: 24vw;
+      left: 50%;
+      transform: translateX(-50%);
+    }
+
+    .tip {
+      font-size: 3vw;
+      color: #b2b2b2;
+      margin-top: 60vw;
+      margin-bottom: 4.8vw;
+      text-align: center;
+    }
+
+    .btn {
+      width: 36vw;
+      height: 8.8vw;
+      background: #bc1724;
+      border-radius: 0.4vw;
+      text-align: center;
+      line-height: 8.8vw;
+      color: #fff;
+      font-size: 3.4vw;
+      margin: 0 auto;
+    }
+
+    .device-list {
+      padding: 0 3.2vw;
+      padding-bottom: 26vw;
+      .device {
+        height: 26vw;
+        background: #f3f5f6;
+        border-radius: 0.8vw;
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        padding: 3.2vw;
+        margin-bottom: 3.2vw;
+        cursor: pointer;
+
+        .cover {
+          img {
+            display: block;
+            width: 19.6vw;
+            height: 19.6vw;
+          }
+        }
+        .content {
+          width: 59vw;
+          margin-left: 3.2vw;
+          .name {
+            font-size: 3.6vw;
+            color: #282828;
+            font-weight: bold;
+            margin-bottom: 3.2vw;
+            @include ellipsis(1);
+          }
+          .sncode {
+            font-size: 3.6vw;
+            color: #666666;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 472 - 0
pages/_template/hyt/center/index.vue

@@ -0,0 +1,472 @@
+<template>
+  <div class="page">
+    <div class="page-top">
+      <SimpleCenterCover />
+      <div class="club-logo">
+        <img :src="clubInfo.logo" alt="" v-if="isAuth" />
+        <img
+          src="~/assets/theme-images/hyt/pc/icon-club-logo-default.png"
+          v-else-if="isPc"
+        />
+        <img
+          src="~/assets/theme-images/hyt/h5/icon-club-logo-default.png"
+          v-else
+        />
+        <div class="club-mobile">{{ userInfo.mobile }}</div>
+        <div class="club-name" v-if="isAuth">
+          机构:{{ clubInfo.authParty }}
+        </div>
+      </div>
+    </div>
+    <div class="page-content">
+      <div class="section-title">我的认证</div>
+      <div class="section-content">
+        <div class="item club" @click="onToClubDetail">
+          <div class="auth-icon" :class="isAuth ? 'auth' : 'un-auth'">
+            {{ isAuth ? '已认证' : '未认证' }}
+          </div>
+          <div class="tip">机构认证</div>
+          <div class="btn">
+            点击查看<span class="icon el-icon-arrow-right"></span>
+          </div>
+        </div>
+        <div class="item device" @click="onToDeviceList">
+          <div class="auth-icon" :class="isAuthDevice ? 'auth' : 'un-auth'">
+            {{ isAuthDevice ? '已认证' : '未认证' }}
+          </div>
+          <div class="tip">设备认证</div>
+          <div class="btn">
+            点击查看<span class="icon el-icon-arrow-right"></span>
+          </div>
+        </div>
+      </div>
+      <div class="line"></div>
+      <template v-if="isPc">
+        <div class="section-title">账户设置</div>
+        <div class="section-content">
+          <div class="reset-pwd" @click="onResetPassword">修改密码</div>
+        </div>
+      </template>
+      <template v-else>
+        <div class="menu-list">
+          <div class="item" @click="toAccountSubNav">
+            <span>账户设置</span>
+            <span class="el-icon-arrow-right"></span>
+          </div>
+        </div>
+      </template>
+      <div v-if="!isPc" class="logout" @click="logout">退出登录</div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-hyt',
+  data() {
+    return {
+      clubInfo: {},
+      list: [],
+    }
+  },
+  computed: {
+    ...mapGetters(['routePrefix', 'isPc', 'userInfo', 'authUserId']),
+    isAuth() {
+      return this.clubInfo.auditStatus === 1
+    },
+    isAuthDevice() {
+      return this.isAuth && this.list.length > 0
+    },
+  },
+  created() {
+    this.initUserInfo()
+  },
+  methods: {
+    // 初始化用户信息
+    async initUserInfo() {
+      try {
+        const res = await this.$http.api.checkTokenResult()
+        this.$store.dispatch('user/login', res.data)
+        this.$setStorage(this.routePrefix, 'userInfo', res.data)
+        this.fetchAuthDetail()
+        this.fetchProductList()
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 获取机构信息
+    async fetchAuthDetail() {
+      try {
+        const authId = this.userInfo.authId
+        if (!authId) return
+        const res = await this.$http.api.fetchClubAuthInfoData({
+          authId,
+        })
+        this.clubInfo = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 获取已认证设备列表
+    async fetchProductList() {
+      try {
+        const authId = this.userInfo.authId
+        if (!authId) return
+        const res = await this.$http.api.fetchClubAuthProductList({
+          authId,
+          authUserId: this.authUserId,
+        })
+        if (res.data) {
+          this.list = res.data
+        }
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 去账户设置菜单列表
+    toAccountSubNav() {
+      this.$router.push(`${this.routePrefix}/center/subnav/account`)
+    },
+    // 机构详情
+    onToClubDetail() {
+      this.$router.push(`${this.routePrefix}/center/club-detail`)
+    },
+    // 设备认证
+    onToDeviceList() {
+      this.$router.push(`${this.routePrefix}/center/device`)
+    },
+    // 修改密码
+    onResetPassword() {
+      this.$router.push(`${this.routePrefix}/center/settings/password`)
+    },
+    // 退出登录
+    logout() {
+      this.$store.dispatch('user/logout')
+      this.$removeStorage(this.routePrefix, 'userInfo')
+      this.backHome()
+    },
+    // 回到首页
+    backHome() {
+      if (this.$route.path === this.routePrefix) return
+      this.$router.replace(this.routePrefix)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+    overflow: hidden;
+  }
+  .page-top {
+    position: relative;
+    height: 360px;
+    // background-image: url(~assets/theme-images/hyt/pc/banner-doc.png);
+
+    .club-logo {
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      text-align: center;
+
+      img {
+        display: inline-block;
+        width: 90px;
+        height: 90px;
+        border-radius: 50%;
+      }
+
+      .club-mobile {
+        margin: 8px 0;
+      }
+
+      .club-mobile,
+      .club-name {
+        font-size: 18px;
+        color: #fff;
+      }
+    }
+  }
+
+  .page-content {
+    width: 704px;
+    margin: 8px auto 60px;
+
+    .section-title {
+      font-size: 24px;
+      color: #666666;
+      padding: 16px 0;
+    }
+    .section-content {
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-start;
+      .item {
+        position: relative;
+        width: 340px;
+        height: 230px;
+        background: #eee;
+        box-sizing: border-box;
+        padding-left: 24px;
+        cursor: pointer;
+
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        justify-content: center;
+        background-size: 340px 230px;
+        background-position: center center;
+        background-repeat: no-repeat;
+
+        &.club {
+          background-image: url(~assets/theme-images/hyt/pc/icon-center-item-auth-club.png);
+          .icon {
+            color: #4093B5;
+          }
+        }
+
+        &.device {
+          background-image: url(~assets/theme-images/hyt/pc/icon-center-item-device.png);
+
+          .icon {
+            color: #0a6eb1;
+          }
+        }
+
+        .auth-icon {
+          position: absolute;
+          right: 16px;
+          top: 16px;
+          width: 72px;
+          height: 32px;
+          line-height: 32px;
+          text-align: center;
+          font-size: 16px;
+          color: #fff;
+          border-radius: 4px;
+
+          &.auth {
+            background: #1890ff;
+          }
+
+          &.un-auth {
+            background: #f94b4b;
+          }
+        }
+        .tip {
+          font-size: 24px;
+          color: #fff;
+          margin-bottom: 8px;
+        }
+        .btn {
+          font-size: 16px;
+          color: #fff;
+
+          .icon {
+            display: inline-block;
+            width: 18px;
+            height: 18px;
+            border-radius: 50%;
+            background: #fff;
+            // background: #0A6EB1;
+            font-size: 12px;
+            text-align: center;
+            line-height: 18px;
+            margin-left: 9px;
+            font-weight: bold;
+            vertical-align: 1px;
+          }
+        }
+      }
+
+      .reset-pwd {
+        width: 98px;
+        height: 36px;
+        background: #4093B5;
+        opacity: 1;
+        border-radius: 4px;
+        font-size: 16px;
+        font-weight: 400;
+        line-height: 36px;
+        color: #ffffff;
+        text-align: center;
+        cursor: pointer;
+      }
+    }
+
+    .line {
+      height: 1px;
+      background: #c2c2c2;
+      margin: 32px 0;
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 36vw;
+    position: relative;
+
+    .club-logo {
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      text-align: center;
+
+      img {
+        display: inline-block;
+        width: 12.8vw;
+        height: 12.8vw;
+        border-radius: 50%;
+      }
+
+      .club-mobile {
+        margin: 0.8vw 0;
+      }
+
+      .club-mobile,
+      .club-name {
+        font-size: 3.4vw;
+        color: #fff;
+      }
+    }
+  }
+
+  .page-content {
+    padding-bottom: 26vw;
+    .section-title {
+      font-size: 4.2vw;
+      color: #666666;
+      padding: 6.4vw 0 3.2vw;
+      padding-left: 4vw;
+    }
+
+    .logout {
+      width: 85.2vw;
+      height: 12vw;
+      background: #4093B5;
+      border-radius: 0.2vw;
+      text-align: center;
+      line-height: 12vw;
+      color: #ffffff;
+      font-size: 3.6vw;
+      position: fixed;
+      bottom: 24vw;
+      left: 50%;
+      transform: translateX(-50%);
+    }
+
+    .section-content {
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-start;
+      padding: 0 4vw;
+      .item {
+        position: relative;
+        width: 44.4vw;
+        height: 30vw;
+        background: #eee;
+        box-sizing: border-box;
+        padding-left: 3.6vw;
+        cursor: pointer;
+
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        justify-content: center;
+        background-size: 44.4vw 30vw;
+        background-position: center center;
+        background-repeat: no-repeat;
+
+        &.club {
+          background-image: url(~assets/theme-images/hyt/pc/icon-center-item-auth-club.png);
+          .icon {
+            color: #4093B5;
+          }
+        }
+
+        &.device {
+          background-image: url(~assets/theme-images/hyt/pc/icon-center-item-device.png);
+          .icon {
+            color: #0a6eb1;
+          }
+        }
+
+        .auth-icon {
+          position: absolute;
+          right: 1.6vw;
+          top: 1.6vw;
+          width: 12.8vw;
+          height: 5.6vw;
+          line-height: 5.6vw;
+          text-align: center;
+          font-size: 3vw;
+          color: #fff;
+          border-radius: 0.4vw;
+
+          &.auth {
+            background: #1890ff;
+          }
+
+          &.un-auth {
+            background: #f94b4b;
+          }
+        }
+        .tip {
+          font-size: 4vw;
+          color: #fff;
+          margin-bottom: 1.2vw;
+        }
+        .btn {
+          font-size: 3vw;
+          color: #fff;
+
+          .icon {
+            display: inline-block;
+            width: 3.4vw;
+            height: 3.4vw;
+            border-radius: 50%;
+            background: #fff;
+            font-size: 2.8vw;
+            text-align: center;
+            line-height: 3.4vw;
+            margin-left: 0.8vw;
+            font-weight: bold;
+            vertical-align: 1px;
+          }
+        }
+      }
+    }
+
+    .menu-list {
+      padding: 0 4vw;
+      padding-top: 2.4vw;
+      .item {
+        width: 100%;
+        padding: 3vw 0;
+        display: flex;
+        justify-content: space-between;
+        font-size: 3.4vw;
+        border-bottom: 0.1vw solid #c2c2c2;
+
+        .el-icon-arrow-right {
+          font-size: 4vw;
+        }
+      }
+    }
+
+    .line {
+      height: 1.6vw;
+      background: #f7f7f7;
+      margin-top: 6.4vw;
+    }
+  }
+}
+</style>

+ 295 - 0
pages/_template/hyt/center/settings/password.vue

@@ -0,0 +1,295 @@
+<template>
+  <div class="page">
+    <div class="page-content">
+      <el-form :model="formData" :rules="rules" label-position="top" ref="form">
+        <el-form-item label="手机号" prop="mobile">
+          <el-input
+            placeholder="请输入手机号"
+            maxlength="11"
+            v-model="formData.mobile"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="验证码" prop="verifyCode">
+          <div class="verify-code">
+            <el-input
+              placeholder="请输入验证码"
+              maxlength="6"
+              v-model="formData.verifyCode"
+            ></el-input>
+            <div class="send" @click="onSend">{{ sendCodeBtnText }}</div>
+          </div>
+        </el-form-item>
+        <el-form-item label="新密码" prop="password">
+          <el-input
+            type="password"
+            placeholder="请输入新密码"
+            v-model="formData.password"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="确认密码" prop="confirmPwd">
+          <el-input
+            type="password"
+            placeholder="请输入确认密码"
+            v-model="formData.confirmPwd"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <div class="submit-button">
+        <div class="back btn" @click="onBack">返回</div>
+        <div class="submit btn" @click="onSubmit">提交</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { isMobile, validPassword } from '~/utils/validator'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-hyt',
+  data() {
+    const passwordValidate = (rule, value, callback) => {
+      console.log(value)
+      if (validPassword(value)) {
+        callback()
+      } else {
+        callback(new Error('密码只能包含英文大小写和数字'))
+      }
+    }
+    const confirmPwdValide = (rule, value, callback) => {
+      if (this.formData.password !== value) {
+        callback(new Error('两次输入的密码不一致'))
+      } else {
+        callback()
+      }
+    }
+
+    return {
+      sendStatus: 0,
+      timer: null,
+      formData: {
+        mobile: '',
+        verifyCode: '',
+        password: '',
+        confirmPwd: '',
+      },
+      rules: {
+        mobile: [
+          { required: true, message: '请输入手机号', trigger: ['blur'] },
+        ],
+        verifyCode: [
+          { required: true, message: '请输入验证码', trigger: ['blur'] },
+        ],
+        password: [
+          { required: true, message: '请输入新密码', trigger: ['blur'] },
+          { min: 6, message: '密码长度不能小于6位', trigger: ['blur'] },
+          { validator: passwordValidate, trigger: ['blur'] },
+        ],
+        confirmPwd: [
+          { required: true, message: '请输入确认密码', trigger: ['blur'] },
+          { validator: confirmPwdValide, trigger: ['blur'] },
+        ],
+      },
+    }
+  },
+  computed: {
+    ...mapGetters(['authUserId', 'routePrefix']),
+    sendCodeBtnText() {
+      return this.sendStatus === 0
+        ? '发送验证码'
+        : `再次发送${this.sendStatus}s`
+    },
+  },
+  methods: {
+    // 返回
+    onBack() {
+      this.$router.back()
+    },
+    // 提交
+    async onSubmit() {
+      try {
+        await this.$refs.form.validate()
+        this.onResetPassword()
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 忘记密码
+    async onResetPassword() {
+      try {
+        await this.$http.api.clubUserReset({
+          mobile: this.formData.mobile,
+          verifyCode: this.formData.verifyCode,
+          password: this.formData.password,
+          authUserId: this.authUserId,
+        })
+        this.$toast('密码修改成功,请重新登录')
+        setTimeout(() => {
+          this.logout()
+        }, 2000)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 退出登录
+    logout() {
+      this.$store.dispatch('user/logout')
+      this.$removeStorage(this.routePrefix, 'userInfo')
+      this.backHome()
+    },
+
+    // 回到首页
+    backHome() {
+      window.location.href = window.location.origin + this.routePrefix
+    },
+
+    // 发送短信验证码
+    async onSend() {
+      if (this.sendStatus > 0) return
+      // 验证手机号是否合法
+      if (!isMobile(this.formData.mobile)) {
+        this.$toast('请输入正确的手机号')
+        return
+      }
+      try {
+        // 发送验证码
+        await this.$http.api.clubUserCodeSend({
+          mobile: this.formData.mobile,
+          authUserId: this.authUserId,
+          type: this.formType === 'register' ? 1 : 2,
+        })
+        this.$toast('验证码已发送')
+        // 开启倒计时
+        this.countdown()
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    countdown() {
+      this.sendStatus = 30
+      this.timer = setInterval(() => {
+        if (this.sendStatus === 0) {
+          clearInterval(this.timer)
+          return
+        }
+        this.sendStatus--
+      }, 1000)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+    overflow: hidden;
+  }
+
+  .page-content {
+    width: 518px;
+    margin: 0 auto;
+    margin-top: 100px;
+
+    .verify-code {
+      width: 100%;
+      display: flex;
+      align-items: center;
+
+      .send {
+        flex-shrink: 0;
+        margin-left: 16px;
+        height: 40px;
+        text-align: center;
+        line-height: 40px;
+        width: 118px;
+        background: #4093B5;
+        border-radius: 4px;
+        font-size: 16px;
+        color: #fff;
+        cursor: pointer;
+      }
+    }
+
+    .submit-button {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-top: 32px;
+      .btn {
+        text-align: center;
+        width: 118px;
+        height: 40px;
+        border-radius: 4px;
+        font-size: 16px;
+        cursor: pointer;
+        box-sizing: border-box;
+        margin: 0 16px;
+
+        &.back {
+          border: 1px solid #4093B5;
+          color: #4093B5;
+          line-height: 38px;
+        }
+        &.submit {
+          background: #4093B5;
+          color: #fff;
+          line-height: 40px;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page-content {
+    padding: 10vw 4vw 0;
+    .verify-code {
+      width: 100%;
+      display: flex;
+      align-items: center;
+
+      .send {
+        flex-shrink: 0;
+        margin-left: 2.4vw;
+        height: 40px;
+        text-align: center;
+        line-height: 40px;
+        width: 24.2vw;
+        background: #4093B5;
+        border-radius: 0.4vw;
+        font-size: 3.4vw;
+        color: #fff;
+        cursor: pointer;
+      }
+    }
+
+    .submit-button {
+      margin-top: 20vw;
+      .btn {
+        text-align: center;
+        height: 40px;
+        border-radius: 0.4vw;
+        font-size: 3.4vw;
+        cursor: pointer;
+        box-sizing: border-box;
+        margin-bottom: 3.2vw;
+
+        &.back {
+          border: 1px solid #4093B5;
+          color: #4093B5;
+          line-height: 38px;
+        }
+        &.submit {
+          background: #4093B5;
+          color: #fff;
+          line-height: 40px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 46 - 0
pages/_template/hyt/center/subnav/account.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="sub-nav">
+    <div class="item" @click="onResetPassword">
+      <span>修改密码</span>
+      <span class="el-icon-arrow-right"></span>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-hyt',
+  data() {
+    return {}
+  },
+  computed: {
+    ...mapGetters(['routePrefix']),
+  },
+  methods: {
+    // 修改密码
+    onResetPassword() {
+      this.$router.push(`${this.routePrefix}/center/settings/password`)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.sub-nav {
+  padding: 0 4vw;
+  padding-top: 2.4vw;
+  .item {
+    width: 100%;
+    padding: 3vw 0;
+    display: flex;
+    justify-content: space-between;
+    font-size: 3.4vw;
+    border-bottom: 0.1vw solid #c2c2c2;
+
+    .el-icon-arrow-right {
+      font-size: 4vw;
+    }
+  }
+}
+</style>

+ 355 - 0
pages/_template/hyt/docs/_fileId.vue

@@ -0,0 +1,355 @@
+<template>
+  <div class="page">
+    <div class="page-top"></div>
+    <div class="page-content">
+      <!-- 面包屑 -->
+      <el-breadcrumb separator-class="el-icon-arrow-right">
+        <el-breadcrumb-item :to="{ path: `${routePrefix}/docs/0` }"
+          >全部文件</el-breadcrumb-item
+        >
+        <template v-for="(item, index) in crumbList">
+          <template v-if="index === crumbList.length - 1">
+            <el-breadcrumb-item :key="item.id">
+              <span class="cell">{{ item.fileName }}</span>
+            </el-breadcrumb-item>
+          </template>
+          <template v-else>
+            <el-breadcrumb-item
+              :key="item.id"
+              :to="{ path: `${routePrefix}/docs/${item.id}` }"
+            >
+              <span>{{ item.fileName | crumbFormat }}</span>
+            </el-breadcrumb-item>
+          </template>
+        </template>
+      </el-breadcrumb>
+      <!-- 列表 -->
+      <div class="list-header">
+        <div class="row">
+          <div class="col">文件名</div>
+          <div class="col">时间</div>
+          <div class="col">大小</div>
+          <div class="col">操作</div>
+        </div>
+      </div>
+      <div class="list-body">
+        <div class="row" v-for="item in list" :key="item.id">
+          <div class="section pc">
+            <div class="col">
+              <doc-icon :type="item.fileType" :src="item.screenshot" />
+              <span
+                class="file-name"
+                v-if="item.fileType === 'article'"
+                @click="onRowClick(item)"
+              >
+                {{ item.fileName | fileNameFormat }}
+              </span>
+              <span class="file-name" v-else @click="onRowClick(item)">{{
+                item.fileName
+              }}</span>
+            </div>
+            <div class="col">{{ item.saveTime | dateFormat }}</div>
+            <div class="col">
+              <span v-if="item.packageType > 0">{{
+                item.fileSize | fileSize
+              }}</span>
+              <span v-else>-</span>
+            </div>
+            <div class="col control">
+              <div class="download" @click="onDownload(item, $event)"></div>
+            </div>
+          </div>
+          <div class="section mobile">
+            <div class="col file-cover">
+              <doc-icon :type="item.fileType" :src="item.screenshot" />
+            </div>
+            <div class="col file-content" @click="onRowClick(item)">
+              <div class="file-name" v-if="item.fileType === 'article'">
+                {{ item.fileName | fileNameFormat }}
+              </div>
+              <div class="file-name" v-else>{{ item.fileName }}</div>
+              <div class="file-info">
+                <span class="date">{{ item.saveTime | dateFormat }}</span>
+                <span class="size">
+                  <span v-if="item.packageType > 0">{{
+                    item.fileSize | fileSize
+                  }}</span>
+                  <span v-else>-</span>
+                </span>
+              </div>
+            </div>
+            <div class="col control">
+              <div class="download" @click="onDownload(item, $event)"></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import fileListMixin from '@/mixins/fileList'
+export default {
+  layout: 'app-hyt',
+  mixins: [fileListMixin],
+}
+</script>
+
+<style lang="scss" scoped>
+/* scss中可以用mixin来扩展 */
+@mixin ellipsis($line: 1) {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: $line;
+  -webkit-box-orient: vertical;
+}
+
+// pc 端
+@media screen and (min-width: 768px) {
+  ::v-deep {
+    .el-breadcrumb {
+      white-space: nowrap;
+      overflow-y: scroll;
+      padding-bottom: 16px;
+
+      .el-breadcrumb__item {
+        float: unset !important;
+      }
+    }
+  }
+  .page {
+    margin-bottom: 16px;
+  }
+  .page-top {
+    height: 360px;
+    background: url(~assets/theme-images/hyt/pc/banner-doc.png);
+    background-size: auto 360px;
+    background-position: center;
+
+    .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;
+    background-color: #fff;
+    margin-top: 16px;
+    box-sizing: border-box;
+    overflow: hidden;
+    padding: 32px 0;
+
+    .el-breadcrumb {
+      margin: 0 24px 32px;
+    }
+
+    .list-header,
+    .list-body {
+      font-size: 14px;
+      color: #282828;
+      .col {
+        &:nth-child(1) {
+          flex: 1;
+        }
+        &:nth-child(2),
+        &:nth-child(3),
+        &:nth-child(4) {
+          width: 120px;
+          text-align: center;
+        }
+      }
+    }
+
+    .list-header {
+      .row {
+        display: flex;
+        justify-content: space-between;
+        padding: 0 24px 16px;
+      }
+    }
+
+    .list-body {
+      .row {
+        line-height: 70px;
+        color: #666666;
+        padding: 0 24px;
+        transition: all 0.4s;
+
+        &:hover {
+          background: #f0fbff;
+        }
+
+        .section {
+          display: flex;
+          justify-content: space-between;
+          border-top: 1px solid #f7f7f7;
+
+          &.mobile {
+            display: none;
+          }
+        }
+
+        img {
+          display: inline-block;
+          width: 32px;
+          height: 32px;
+          background: #eee;
+          margin-right: 24px;
+        }
+
+        .file-name {
+          cursor: pointer;
+          transition: all 0.4s;
+          margin-left: 24px;
+          &:hover {
+            color: #4093B5;
+          }
+        }
+
+        .control {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          .download {
+            display: block;
+            width: 24px;
+            height: 24px;
+            background: url(~assets/theme-images/common/pc-icon-download.png)
+              no-repeat center;
+            background-size: 24px;
+            margin: 0 auto;
+            cursor: pointer;
+
+            &:hover {
+              background-image: url(~assets/theme-images/hyt/pc/icon-download-hover.png);
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  ::v-deep {
+    .el-breadcrumb {
+      white-space: nowrap;
+      overflow-y: scroll;
+      padding-bottom: 2.4vw;
+
+      .el-breadcrumb__item {
+        float: unset !important;
+      }
+    }
+  }
+  .page-top {
+    height: 46vw;
+    background: url(~assets/theme-images/hyt/h5/banner-doc.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+      text-align: center;
+      margin: 0 3.2vw;
+    }
+  }
+
+  .page-content {
+    position: relative;
+    padding: 8vw 0;
+
+    .el-breadcrumb {
+      margin: 0 4vw 4vw;
+    }
+
+    .list-header {
+      display: none;
+    }
+
+    .list-body {
+      font-size: 3.6vw;
+      color: #282828;
+
+      .row {
+        color: #666666;
+        padding: 0 4vw;
+
+        .section {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          border-bottom: 0.1vw solid #f7f7f7;
+          height: 17.6vw;
+
+          &.pc {
+            display: none;
+          }
+
+          .file-cover {
+            img {
+              width: 8.8vw;
+              height: 8.8vw;
+              background: #eee;
+            }
+          }
+
+          .file-content {
+            flex: 1;
+            margin: 0 4.8vw;
+
+            .file-name {
+              font-size: 3.6vw;
+              @include ellipsis(1);
+            }
+
+            .file-info {
+              font-size: 3vw;
+              color: #999999;
+              margin-top: 1.6vw;
+
+              .size {
+                margin-left: 4vw;
+              }
+            }
+          }
+
+          .control {
+            width: 6.4vw;
+            height: 6.4vw;
+            .download {
+              display: block;
+              width: 6.4vw;
+              height: 6.4vw;
+              background: url(~assets/theme-images/common/h5-icon-download.png)
+                no-repeat center;
+              background-size: 6.4vw;
+              margin: 0 auto;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 83 - 0
pages/_template/hyt/docs/article-detail.vue

@@ -0,0 +1,83 @@
+<template>
+  <div class="page">
+    <div class="page-top">
+      <div class="title" v-text="articleInfo.articleTitle"></div>
+      <div class="date">{{ articleInfo.createTime | dateFormat }}</div>
+    </div>
+    <div class="page-content" v-html="html"></div>
+  </div>
+</template>
+
+<script>
+import articleDetailMixin from '@/mixins/articleDetail'
+export default {
+  layout: 'app-hyt',
+  mixins: [articleDetailMixin],
+}
+</script>
+
+<style scoped lang="scss">
+/* scss中可以用mixin来扩展 */
+@mixin ellipsis($line: 1) {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: $line;
+  -webkit-box-orient: vertical;
+}
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    width: 1200px;
+    margin: 24px auto;
+    box-sizing: border-box;
+    background: #fff;
+    padding: 24px;
+  }
+  .page-top {
+    padding-bottom: 24px;
+    border-bottom: 1px solid #d8d8d8;
+    .title {
+      font-size: 28px;
+      color: #101010;
+      line-height: 1.6;
+      text-align: justify;
+    }
+    .date {
+      font-size: 18px;
+      color: #b2b2b2;
+      margin-top: 24px;
+    }
+  }
+  .page-content {
+    padding-top: 24px;
+    color: #404040;
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page {
+    box-sizing: border-box;
+    background: #fff;
+    padding: 4vw;
+  }
+  .page-top {
+    .title {
+      font-size: 4.2vw;
+      color: #101010;
+      line-height: 1.6;
+      text-align: justify;
+    }
+    .date {
+      font-size: 3.2vw;
+      color: #b2b2b2;
+      margin-top: 4vw;
+    }
+  }
+  .page-content {
+    padding-top: 24px;
+    color: #404040;
+  }
+}
+</style>

+ 242 - 0
pages/_template/hyt/docs/detail.vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="page-content">
+    <div v-if="fileData" class="doc-header">
+      <div class="info">
+        <div class="cover">
+          <doc-icon :type="fileData.fileType" :width="32" :height="32" />
+        </div>
+        <div class="content">
+          <div class="name">{{ fileData.fileName }}</div>
+          <div class="row">
+            <span class="date">时间:{{ fileData.saveTime | dateFormat }}</span>
+            <span class="size">大小:{{ fileData.fileSize | fileSize }}</span>
+          </div>
+        </div>
+      </div>
+      <div>
+        <div @click="onDownload(fileData, $event)" class="download"></div>
+      </div>
+    </div>
+    <div class="doc-content">
+      <img v-if="fileType === 'image'" :src="fileData.ossUrl" alt="" />
+      <video v-else-if="fileType === 'video'" :src="fileData.ossUrl" controls />
+      <div v-else class="other">
+        <doc-icon type="icon-other" :width="64" :height="64" />
+        <div class="name">{{ fileData.fileName }}</div>
+        <div class="download" @click="onDownload(fileData, $event)">
+          下载并打开
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import fileDetailMixin from '@/mixins/fileDetail'
+export default {
+  layout: 'app-hyt',
+  mixins: [fileDetailMixin],
+}
+</script>
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page-content {
+    width: 1200px;
+    margin: 0 auto;
+    background: #fff;
+    margin-top: 16px;
+    padding: 24px;
+    box-sizing: border-box;
+  }
+  .doc-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #e8e8e8;
+    padding-bottom: 26px;
+
+    .download {
+      width: 24px;
+      height: 24px;
+      background: url(~assets/theme-images/common/pc-icon-download.png)
+        no-repeat center;
+      background-size: 24px;
+      cursor: pointer;
+
+      &:hover {
+        background-image: url(~assets/theme-images/common/pc-icon-download-hover.png);
+      }
+    }
+
+    .info {
+      display: flex;
+      align-items: center;
+
+      .cover {
+        img {
+          display: block;
+          width: 48px;
+          height: 48px;
+          background: skyblue;
+        }
+      }
+
+      .content {
+        margin-left: 24px;
+        .name {
+          font-size: 16px;
+          color: #282828;
+          margin-bottom: 8px;
+        }
+        .date,
+        .size {
+          font-size: 14px;
+          color: #999999;
+        }
+
+        .size {
+          margin-left: 32px;
+        }
+      }
+    }
+  }
+
+  .doc-content {
+    display: flex;
+    justify-content: center;
+    img,
+    video {
+      display: block;
+      max-height: 540px;
+      // background: #000;
+      margin-top: 40px;
+      margin-bottom: 24px;
+    }
+
+    .other {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+      height: 610px;
+
+      .name {
+        font-size: 16px;
+        color: #666666;
+        margin: 16px 0 24px;
+      }
+
+      .download {
+        width: 88px;
+        height: 36px;
+        background: #BC1724;
+        opacity: 1;
+        border-radius: 2px;
+        text-align: center;
+        line-height: 36px;
+        color: #ffffff;
+        font-size: 14px;
+        cursor: pointer;
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page-content {
+    background: #fff;
+    box-sizing: border-box;
+    padding: 4vw;
+  }
+  .doc-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 0.1vw solid #e8e8e8;
+    padding-bottom: 4vw;
+
+    .download {
+      width: 6.4vw;
+      height: 6.4vw;
+      background: url(~assets/theme-images/common/pc-icon-download.png)
+        no-repeat center;
+      background-size: 6.4vw;
+      cursor: pointer;
+
+      &:hover {
+        background-image: url(~assets/theme-images/common/pc-icon-download-hover.png);
+      }
+    }
+
+    .info {
+      display: flex;
+      align-items: center;
+
+      .cover {
+        img {
+          display: block;
+          width: 12vw;
+          height: 12vw;
+          background: skyblue;
+        }
+      }
+
+      .content {
+        margin-left: 4vw;
+        .name {
+          font-size: 3.6vw;
+          color: #282828;
+          margin-bottom: 1vw;
+        }
+        .date,
+        .size {
+          font-size: 3vw;
+          color: #999999;
+        }
+
+        .size {
+          margin-left: 4vw;
+        }
+      }
+    }
+  }
+
+  .doc-content {
+    display: flex;
+    justify-content: center;
+    img,
+    video {
+      display: block;
+      width: 100%;
+      // background: #000;
+      margin-top: 4vw;
+    }
+
+    .other {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+      height: 100vw;
+
+      .name {
+        font-size: 3.4vw;
+        color: #666666;
+        margin: 2.4vw 0 6.4vw;
+      }
+
+      .download {
+        width: 32vw;
+        height: 8.8vw;
+        background: #BC1724;
+        opacity: 1;
+        border-radius: 0.2vw;
+        text-align: center;
+        line-height: 8.8vw;
+        color: #ffffff;
+        font-size: 3.4vw;
+        cursor: pointer;
+      }
+    }
+  }
+}
+</style>

+ 221 - 0
pages/_template/hyt/feedback/index.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="page">
+    <div class="page-top"></div>
+    <div class="page-content p-4 md:my-4">
+      <textarea
+        class="control p-2"
+        placeholder="请在此处输入您的宝贵意见(限200字)"
+        v-model="content"
+      ></textarea>
+      <div class="submit mt-6" @click="onSubmit">提交</div>
+    </div>
+
+    <van-dialog v-model="showModal" class="dialog" @confirm="onConfirm" confirm-button-color="#4093b5">
+      <div class="dialog-content">
+        <div class="image-icon"></div>
+        <div class="title">提交成功</div>
+        <div class="tip">您的反馈信息已提交,感谢您的宝贵意见。</div>
+        <div class="line" />
+      </div>
+    </van-dialog>
+  </div>
+</template>
+
+<script>
+import feedbackMixin from '@/mixins/feedback'
+export default {
+  layout: 'app-hyt',
+  mixins: [feedbackMixin],
+}
+</script>
+
+<style scoped lang="scss">
+// pc 端
+@media screen and (min-width: 768px) {
+  .page-top {
+    height: 360px;
+    background: url(~assets/theme-images/hyt/pc/banner-feedback.png);
+    background-size: auto 360px;
+    background-position: center;
+
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1200px;
+    margin-left: auto;
+    margin-right: auto;
+    box-sizing: border-box;
+    background-color: #fff;
+    .control {
+      display: block;
+      width: 100%;
+      height: 280px;
+      border: 1px solid #d8d8d8;
+      outline: none;
+      box-sizing: border-box;
+      font-size: 16px;
+      color: #101010;
+    }
+
+    .submit {
+      text-align: center;
+      line-height: 46px;
+      width: 326px;
+      height: 46px;
+      margin-left: auto;
+      margin-right: auto;
+      border-radius: 4px;
+      font-size: 16px;
+      background-color: #4093b5;
+      color: #fff;
+      transition: all 0.2s;
+      cursor: pointer;
+
+      &:hover {
+        background-color: #3586a7;
+      }
+
+      &.disabled {
+        background-color: #d8d8d8 !important;
+      }
+    }
+  }
+
+  .dialog {
+    width: 380px;
+    padding-top: 40px;
+    border-radius: 0;
+
+    .dialog-content {
+      width: 100%;
+      height: 100%;
+
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+
+      .title {
+        font-size: 24px;
+        color: #101010;
+        margin: 28px 0 12px;
+      }
+
+      .tip {
+        color: #404040;
+        font-size: 16px;
+      }
+
+      .line {
+        width: 340px;
+        height: 1px;
+        margin: 0 auto;
+        margin-top: 28px;
+        background: #d8d8d8;
+      }
+
+      .image-icon {
+        width: 140px;
+        height: 100px;
+        background: url(~assets/theme-images/hyt/pc/icon-feedback-submit.png)
+          no-repeat center;
+        background-size: 140px 100px;
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    background: url(~assets/theme-images/hyt/pc/banner-feedback.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+  .page-content {
+    .control {
+      display: block;
+      width: 100%;
+      height: 56vw;
+      border: 0.1vw solid #d8d8d8;
+      outline: none;
+      box-sizing: border-box;
+      font-size: 3.2vw;
+      color: #101010;
+    }
+
+    .submit {
+      text-align: center;
+      line-height: 11.6vw;
+      width: 100%;
+      height: 11.6vw;
+      border-radius: 0.2vw;
+      font-size: 4vw;
+      background-color: #4093b5;
+      color: #fff;
+
+      &.disabled {
+        background-color: #d8d8d8;
+      }
+    }
+  }
+
+  .dialog {
+    width: 76vw;
+    border-radius: 0;
+
+    .dialog-content {
+      padding-top: 3.2vw;
+      width: 100%;
+      height: 100%;
+
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+
+      .title {
+        font-size: 4.6vw;
+        color: #101010;
+        margin: 3.2vw 0;
+      }
+
+      .tip {
+        color: #404040;
+        font-size: 3.2vw;
+      }
+
+      .image-icon {
+        width: 30vw;
+        height: 22vw;
+        background: url(~assets/theme-images/hyt/pc/icon-feedback-submit.png)
+          no-repeat center;
+        background-size: 30vw 20vw;
+      }
+    }
+  }
+}
+</style>

+ 514 - 0
pages/_template/hyt/form/club-bind.vue

@@ -0,0 +1,514 @@
+<template>
+  <div class="club-bind">
+    <div class="form-container">
+      <div class="logo">
+        <img :src="supplierInfo.shopName" alt="" />
+        <!-- <img :src="supplierInfo.logo" :alt="supplierInfo.shopName" /> -->
+      </div>
+      <div class="tip">
+        输入验证码即可完成账号注册及{{
+          supplierInfo.shopName
+        }}授权牌匾制作与寄送
+      </div>
+      <div class="form">
+        <el-form
+          :model="formData"
+          :rules="rules"
+          label-position="top"
+          ref="formRef"
+        >
+          <el-form-item label="手机号:" prop="mobile">
+            <el-input
+              v-model="formData.mobile"
+              maxlength="11"
+              @input="handleMobileInput"
+            />
+          </el-form-item>
+          <el-form-item label="验证码:" prop="verifyCode">
+            <el-input v-model="formData.verifyCode" maxlength="6">
+              <template #suffix>
+                <i class="line"></i>
+                <el-button
+                  type="text"
+                  size="mini"
+                  class="code"
+                  v-text="sendCodeBtnText"
+                  @click="onSend"
+                ></el-button>
+              </template>
+            </el-input>
+          </el-form-item>
+        </el-form>
+        <div id="slide-verify" class="verify-wrap"></div>
+        <el-button type="primary" class="confirm" @click="onSubmit"
+          >确定</el-button
+        >
+      </div>
+    </div>
+
+    <el-dialog
+      title="提示"
+      :visible.sync="dialogVisible"
+      center
+      @closed="onDialogClosed"
+    >
+      <div class="dialog-text text-center" v-text="dialogText"></div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="onCancel" v-if="resultStatus === -2">取消</el-button>
+        <el-button type="primary" @click="onConfirm">{{
+          resultStatus === -2 ? '去登录' : '确定'
+        }}</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { mapGetters } from 'vuex'
+import { isMobile } from '~/utils/validator'
+import { SlideVerify } from '@/utils/libs/slide-verify'
+export default {
+  layout: 'app-hyt',
+  data() {
+    var validateMobile = (rule, value, callback) => {
+      if (!value || isMobile(value)) {
+        callback()
+      } else {
+        callback(new Error('手机号格式不正确'))
+      }
+    }
+
+    return {
+      resultStatus: 0,
+      dialogVisible: false,
+      slideVerifyStatus: false,
+      slideVerify: null,
+      formData: {
+        mobile: '',
+        verifyCode: '',
+        authId: '',
+        authUserId: '',
+      },
+      rules: {
+        mobile: [
+          { required: true, message: '手机号不能为空', trigger: ['blur'] },
+          { validator: validateMobile, trigger: ['blur'] },
+        ],
+        verifyCode: [
+          { required: true, message: '验证码不能为空', trigger: ['blur'] },
+        ],
+      },
+      sendStatus: 0,
+      timer: null,
+      dialogTextMap: {
+        0: '账号注册成功,您可通过该手机号与短信发送的密码登录认证通!',
+        '-2': '抱歉,该手机号已注册,您可以登录后再来进行正品授权申请!',
+      },
+    }
+  },
+  computed: {
+    ...mapGetters(['authUserId', 'routePrefix', 'supplierInfo']),
+    sendCodeBtnText() {
+      return this.sendStatus === 0
+        ? '发送验证码'
+        : `再次发送${this.sendStatus}s`
+    },
+    dialogText() {
+      return this.dialogTextMap[this.resultStatus]
+    },
+  },
+  created() {
+    this.$store.commit('app/HIDE_LAYOUT')
+    this.initLinkInfo()
+    this.initSlideVerify()
+  },
+  beforeDestroy() {
+    this.$store.commit('app/SHOW_LAYOUT')
+  },
+  methods: {
+    initSlideVerify() {
+      this.$nextTick(() => {
+        this.slideVerify = new SlideVerify('#slide-verify', {
+          initText: '请向右滑动滑块', //设置  初始的 显示文字
+          sucessText: '验证通过', //设置 验证通过 显示的文字
+          getSuccessState: (status) => {
+            //当验证完成的时候 会 返回 res 值 true,只留了这个应该够用了
+            this.slideVerifyStatus = status
+          },
+        })
+      })
+    },
+
+    // 取消操作
+    onCancel() {
+      this.dialogVisible = false
+    },
+
+    // 确认
+    onConfirm() {
+      this.$router.replace(this.routePrefix)
+    },
+
+    onDialogClosed() {
+      if (this.resultStatus === 0) {
+        this.$router.replace(this.routePrefix)
+      }
+    },
+
+    // 初始化链接信息
+    async initLinkInfo() {
+      this.formData.authUserId = this.authUserId
+      const authId = this.$route.query.authId
+      if (authId) {
+        this.formData.authId = parseInt(authId)
+        return
+      }
+      try {
+        await this.$alert('链接已失效!请更换新链接', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        })
+      } finally {
+        this.$router.replace(this.routePrefix)
+      }
+    },
+    // 提交
+    async onSubmit() {
+      if (!this.slideVerifyStatus) {
+        return this.$toast('滑动验证未通过')
+      }
+      try {
+        await this.$refs.formRef.validate()
+        this.onRegisterSubmit()
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.slideVerifyStatus = false
+        setTimeout(() => {
+          this.slideVerify?.resetVerify()
+        }, 500)
+      }
+    },
+    // 用户注册
+    async onRegisterSubmit() {
+      try {
+        await this.$http.api.clubUserRegister(this.formData)
+        this.resultStatus = 0
+        this.dialogVisible = true
+      } catch (error) {
+        if (error.code === -2) {
+          this.resultStatus = -2
+          this.dialogVisible = true
+        } else {
+          this.$toast(error.msg)
+        }
+      }
+    },
+
+    // 发送验证码
+    async onSend() {
+      if (this.sendStatus > 0) return
+      // 验证手机号是否合法
+      if (!isMobile(this.formData.mobile)) {
+        this.$toast('请输入正确的手机号')
+        return
+      }
+      try {
+        // 发送验证码
+        await this.$http.api.clubUserCodeSend({
+          mobile: this.formData.mobile,
+          authUserId: this.authUserId,
+          type: 1,
+        })
+        this.$toast('验证码已发送')
+        // 开启倒计时
+        this.countdown()
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 倒计时
+    countdown() {
+      this.sendStatus = 60
+      this.timer = setInterval(() => {
+        if (this.sendStatus === 0) {
+          clearInterval(this.timer)
+          return
+        }
+        this.sendStatus--
+      }, 1000)
+    },
+    // 输入框输入时
+    handleMobileInput() {
+      this.formData.mobile = this.formData.mobile.replace(/[^\w\.\/]/gi, '')
+    },
+  },
+}
+</script>
+<style lang="scss" scoped>
+.el-button {
+  border-radius: 0;
+}
+
+@media screen and (min-width: 768px) {
+  ::v-deep {
+    .el-dialog {
+      width: 400px;
+      padding: 40px 36px 32px;
+
+      .dialog-text {
+        font-size: 16px;
+        color: #282828;
+        line-height: 32px;
+      }
+
+      .el-dialog__header {
+        padding: 0;
+        .el-dialog__title {
+          font-size: 24px;
+          color: #282828;
+        }
+      }
+
+      .el-dialog__body {
+        padding: 64px 0 48px;
+      }
+
+      .el-dialog__footer {
+        padding: 0;
+      }
+
+      .el-dialog__close {
+        font-size: 24px;
+      }
+
+      .el-button {
+        width: 100%;
+        display: block;
+
+        &:first-child {
+          margin-bottom: 10px;
+        }
+
+        &:last-child {
+          margin-left: 0;
+        }
+      }
+    }
+  }
+
+  .club-bind {
+    width: 100vw;
+    height: 100vh;
+    background: url(~assets/theme-images/common/pc-link-register-bg.png)
+      no-repeat center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .form-container {
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      box-sizing: border-box;
+      width: 528px;
+      padding: 32px 0 60px;
+      background: #fff;
+      box-shadow: 0px 6px 30px rgba(64, 158, 255, 0.1);
+      border-radius: 4px;
+
+      .logo {
+        width: 248px;
+        height: 100px;
+        margin-bottom: 20px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        img {
+          display: block;
+          max-height: 100%;
+          flex-shrink: 0;
+        }
+      }
+
+      .tip {
+        width: 412px;
+        font-size: 20px;
+        color: #282828;
+        line-height: 32px;
+        text-align: center;
+        margin-bottom: 36px;
+      }
+
+      .form {
+        width: 320px;
+
+        .confirm {
+          width: 100%;
+          margin-top: 24px;
+          border-radius: 0;
+        }
+
+        .line {
+          width: 1px;
+          height: 20px;
+          background: #d8d8d8;
+          display: inline-block;
+          vertical-align: middle;
+        }
+
+        .code {
+          margin: 0 10px;
+        }
+
+        .el-form-item {
+          margin-bottom: 24px;
+
+          ::v-deep {
+            .el-form-item__label {
+              line-height: initial;
+              padding: 0 0 6px;
+            }
+
+            .el-input__inner {
+              border-radius: 0;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  ::v-deep {
+    .el-dialog {
+      width: 76vw;
+      padding: 7vw 7vw 5.2vw;
+
+      .dialog-text {
+        font-size: 3.2vw;
+        color: #282828;
+        line-height: 5.6vw;
+      }
+
+      .el-dialog__header {
+        padding: 0;
+        .el-dialog__title {
+          font-size: 4.8vw;
+          color: #282828;
+        }
+      }
+
+      .el-dialog__body {
+        padding: 8vw 0 11.6vw;
+      }
+
+      .el-dialog__footer {
+        padding: 0;
+      }
+
+      .el-dialog__close {
+        font-size: 5.6vw;
+      }
+
+      .el-button {
+        width: 100%;
+        display: block;
+
+        &:first-child {
+          margin-bottom: 4vw;
+        }
+
+        &:last-child {
+          margin-left: 0;
+        }
+      }
+    }
+  }
+
+  .club-bind {
+    width: 100vw;
+    height: 100vh;
+    background: url(~assets/theme-images/common/h5-link-register-bg.png)
+      no-repeat center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .form-container {
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      box-sizing: border-box;
+      width: 76vw;
+      padding: 4vw 0 4.7vw;
+      background: #fff;
+      box-shadow: 0px 0.6vw 3vw rgba(64, 158, 255, 0.1);
+      border-radius: 4px;
+
+      .logo {
+        width: 34vw;
+        height: 13.8vw;
+        margin-bottom: 4vw;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        img {
+          display: block;
+          height: 5.6vw;
+          flex-shrink: 0;
+        }
+      }
+
+      .tip {
+        width: 59.8vw;
+        font-size: 3.6vw;
+        color: #282828;
+        line-height: 5.6vw;
+        text-align: center;
+        margin-bottom: 8vw;
+      }
+
+      .form {
+        width: 62vw;
+
+        .confirm {
+          width: 100%;
+          margin-top: 3.2vw;
+          border-radius: 0;
+        }
+
+        .line {
+          width: 1px;
+          height: 4vw;
+          background: #d8d8d8;
+          display: inline-block;
+          vertical-align: middle;
+        }
+
+        .code {
+          margin: 0 2.8vw;
+        }
+
+        .el-form-item {
+          margin-bottom: 4vw;
+
+          ::v-deep {
+            .el-form-item__label {
+              line-height: initial;
+              padding: 0 0 1.2vw;
+            }
+
+            .el-input__inner {
+              border-radius: 0;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,527 @@
+<template>
+  <div class="page">
+    <div class="page-top"></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-hyt',
+  components: {
+    SimpleStep,
+    FormClubRegister,
+    FormClubInfo,
+    FormClubDevice,
+  },
+
+  data() {
+    return {
+      isRequest: true,
+      active: false,
+      isSubmit: 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()
+  },
+  beforeDestroy() {
+    this.$toast.clear()
+    this.isSubmit = false
+  },
+  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('userForm', this.clubUserInfo)
+    },
+    onPrevStep() {
+      this.step > 1 && this.step--
+    },
+
+    async onSubmit() {
+      this.$toast.loading({
+        message: '正在提交注册信息,请勿操作...',
+        duration: 0,
+      })
+      if (this.isSubmit) return
+      this.isSubmit = true
+      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)
+      } finally {
+        this.$toast.clear()
+        this.isSubmit = false
+      }
+    },
+
+    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('请登录后查看')
+        setTimeout(() => {
+          this.$router.push(`${this.routePrefix}`)
+        }, 1500)
+        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;
+    background: url(~assets/theme-images/hyt/pc/banner-register.png);
+    background-size: auto 360px;
+    background-position: center;
+
+    .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(~assets/theme-images/common/icon-auth-primary.png);
+        }
+        &.warning {
+          background-image: url(~assets/theme-images/common/icon-auth-warning.png);
+        }
+        &.danger {
+          background-image: url(~assets/theme-images/common/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;
+        border-radius: 4px;
+
+        cursor: pointer;
+
+        &.prev {
+          border: 1px solid #4093B5;
+          color: #4093B5;
+        }
+        &.next {
+          background-color: #4093B5;
+          color: #fff;
+        }
+      }
+      .record {
+        font-size: 14px;
+        cursor: pointer;
+        color: #4093B5;
+      }
+    }
+
+    .step-list {
+      width: 700px;
+      margin: 0 auto;
+    }
+  }
+}
+
+// 移动端
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    background: url(~assets/theme-images/hyt/h5/banner-register.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .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(~assets/theme-images/common/icon-auth-primary.png);
+        }
+        &.warning {
+          background-image: url(~assets/theme-images/common/icon-auth-warning.png);
+        }
+        &.danger {
+          background-image: url(~assets/theme-images/common/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;
+        border-radius: 4px;
+
+        cursor: pointer;
+
+        &.prev {
+          border: 1px solid #4093B5;
+          color: #4093B5;
+        }
+        &.next {
+          background-color: #4093B5;
+          color: #fff;
+        }
+      }
+      .record {
+        margin-top: 4.8vw;
+        font-size: 3.4vw;
+        cursor: pointer;
+        color: #4093B5;
+      }
+    }
+  }
+}
+</style>

+ 685 - 0
pages/_template/hyt/form/components/form-club-device.vue

@@ -0,0 +1,685 @@
+<template>
+  <div class="club-device">
+    <template v-for="(formItem, index) 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 label="认证方式:">
+            <el-radio-group
+              v-model="formItem.authType"
+              @change="onAuthTypeChange(formItem, index)"
+            >
+              <el-radio :label="1">新设备认证</el-radio>
+              <el-radio :label="2">关联已认证设备</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <template
+            v-if="
+              formItem.authType === 1 ||
+              (formItem.authType === 2 && formItem.snCode)
+            "
+          >
+            <el-form-item
+              prop="productName"
+              :label="`设备名称${formItem.uuid}:`"
+            >
+              <el-select
+                v-model="formItem.productName"
+                filterable
+                allow-create
+                placeholder="请输入新设备名称或选择已有设备"
+                @change="onProductNameChange(formItem, $event)"
+                clearable
+                :disabled="formItem.authType === 2"
+              >
+                <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="infoId" v-if="editPrams">
+              <el-select
+                v-model="formItem.infoId"
+                placeholder="请选择品牌"
+                :disabled="formItem.authType === 2"
+              >
+                <el-option
+                  v-for="item in brandList"
+                  :key="item.infoId"
+                  :label="item.brandName"
+                  :value="item.infoId"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item prop="purchaseWay" label="购买渠道:">
+              <el-input
+                placeholder="请输入购买渠道"
+                v-model="formItem.purchaseWay"
+                :disabled="formItem.authType === 2"
+              ></el-input>
+            </el-form-item>
+            <el-form-item prop="invoiceImage" label="发票:">
+              <br />
+              <el-input
+                v-show="false"
+                v-model="formItem.invoiceImage"
+              ></el-input>
+              <SimpleUploadImage
+                :disabled="formItem.authType === 2"
+                :limit="1"
+                :image-list="formItem.invoiceImageList"
+                :before-upload="beforeInvoiceImageUpload"
+                @success="uploadInvoiceImageSuccess(formItem, $event)"
+                @remove="handleInvoiceImageRemove(formItem, $event)"
+              />
+            </el-form-item>
+          </template>
+          <el-form-item prop="snCode" :label="`设备SN码${formItem.uuid}:`">
+            <el-input
+              :placeholder="
+                formItem.authType === 1
+                  ? '请输入设备SN码'
+                  : '请输入已认证过的设备SN码'
+              "
+              @blur="onSnCodeBlur(formItem)"
+              v-model.lazy="formItem.snCode"
+            ></el-input>
+          </el-form-item>
+          <template v-if="formItem.authType === 1">
+            <el-form-item prop="paramList" label="设备参数:" v-if="editPrams">
+              <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>
+          </template>
+        </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) => {
+      const notEmptyList = value.filter(
+        (item) => item.paramName.trim() && item.paramContent.trim()
+      )
+      if (notEmptyList.length === 0) {
+        callback(new Error('参数列表不能为空'))
+      } else if (notEmptyList.length < 4) {
+        callback(new Error('请填写至少4项参数'))
+      } 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'] },
+        ],
+        infoId: [
+          { 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'] },
+        ],
+        authType: [
+          { required: true, message: '请选择认证方式', trigger: ['change'] },
+        ],
+      },
+      formList: [],
+      brandList: [],
+      deviceList: [],
+      editPrams: true,
+    }
+  },
+
+  computed: {
+    ...mapGetters(['authUserId']),
+  },
+
+  created() {
+    this.fetchBrandList()
+    this.fetchDeviceList()
+    this.initFormList()
+  },
+
+  methods: {
+    // sn码输入框是去焦点
+    async onSnCodeBlur(formItem) {
+      if (formItem.authType === 1) return
+      if (!formItem.snCode) return
+      try {
+        const res = await this.$http.api.fetchDetialBySnCode({
+          snCode: formItem.snCode,
+        })
+        formItem.invoiceImage = res.data.invoiceImage
+        formItem.purchaseWay = res.data.purchaseWay
+        formItem.productTypeId = res.data.productTypeId
+        formItem.productName = res.data.productName
+        formItem.infoId = res.data.infoId
+        formItem.productImage = res.data.productImage
+        if (res.data.invoiceImage) {
+          formItem.invoiceImageList = [
+            { name: '发票', url: res.data.invoiceImage },
+          ]
+        }
+        if (res.data.productImage) {
+          formItem.productImageList = [
+            { name: '产品图片', url: res.data.productImage },
+          ]
+        }
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    // 认证方式切换
+    onAuthTypeChange(formItem, index) {
+      formItem.purchaseWay = ''
+      formItem.invoiceImage = ''
+      formItem.productName = ''
+      formItem.productTypeId = ''
+      formItem.productImageList = []
+      formItem.invoiceImageList = []
+      formItem.paramList = this.initParams()
+      this.$refs.form[index]?.resetFields()
+    },
+
+    // 表单验证
+    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.authType = formData.authType
+      obj.relationId = formData.relationId || ''
+      obj.productImageList = productImageList
+      obj.invoiceImageList = invoiceImageList
+      obj.productImage = formData.productImage
+      obj.productName = formData.productName
+      obj.snCode = formData.snCode
+      obj.infoId = formData.infoId
+      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.authType = formItem.authType
+        obj.productName = formItem.productName
+        obj.snCode = formItem.snCode
+        obj.infoId = formItem.infoId
+        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码
+        infoId: '',
+        productTypeId: '',
+        purchaseWay: '', // 购买渠道
+        invoiceImage: '', // 发票
+        productImageList: [],
+        invoiceImageList: [],
+        authType: 1,
+      }
+    },
+
+    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 }]
+        this.editPrams = false
+      } else {
+        formItem.productTypeId = ''
+        formItem.productImage = ''
+        formItem.productImageList = []
+        this.editPrams = true
+      }
+    },
+
+    // 获取品牌列表
+    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>
+.club-device {
+  ::v-deep {
+    .el-input.is-active .el-input__inner,
+    .el-input__inner:focus {
+      border-color: #4093B5;
+    }
+    .el-radio__input.is-checked + .el-radio__label {
+      color: #4093B5;
+    }
+    .el-radio__input.is-checked .el-radio__inner {
+      background-color: #4093B5;
+      border-color: #4093B5;
+    }
+  }
+}
+
+// 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;
+      color: #4093B5;
+    }
+
+    .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;
+
+    border: 1px solid #4093B5;
+    color: #4093B5;
+
+    .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;
+        background: #4093B5;
+      }
+      &::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;
+      color: #4093B5;
+    }
+
+    .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;
+    border: 1px solid #4093B5;
+    color: #4093B5;
+
+    .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;
+        background: #4093B5;
+      }
+      &::after {
+        transform: translate(-50%, -50%) rotateZ(90deg);
+      }
+    }
+  }
+}
+</style>

+ 845 - 0
pages/_template/hyt/form/components/form-club-info.vue

@@ -0,0 +1,845 @@
+<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="linkMan" label="运营联系人:">
+        <el-input
+          placeholder="请输入运营联系人"
+          v-model="formData.linkMan"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="linkMobile" label="运营联系人手机号:">
+        <el-input
+          placeholder="请输入运营联系人手机号"
+          v-model="formData.linkMobile"
+        ></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: '',
+        linkMan: '',
+        linkMobile: '',
+      },
+      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: [],
+    }
+  },
+
+  watch: {
+    'formData.firstClubType': function (nVal, oVal) {
+      if (nVal === 1) {
+        this.formData.secondClubType = 1
+      } else if (nVal === 2) {
+        this.formData.secondClubType = 5
+      } else {
+        this.formData.secondClubType = ''
+      }
+    },
+  },
+  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,
+        linkMan: this.formData.linkMan,
+        linkMobile: this.formData.linkMobile,
+        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.formData.linkMan = formData.linkMan
+      this.formData.linkMobile = formData.linkMobile
+      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>
+.club-info {
+  ::v-deep {
+    .el-input.is-active .el-input__inner,
+    .el-input__inner:focus {
+      border-color: #4093B5;
+    }
+  }
+}
+
+// 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(~assets/theme-images/common/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(~assets/theme-images/common/icon-position.png) no-repeat
+          center;
+        background-size: 3.58vw;
+      }
+    }
+  }
+}
+</style>

+ 226 - 0
pages/_template/hyt/form/components/form-club-register.vue

@@ -0,0 +1,226 @@
+<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"
+          @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"
+            @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>
+.club-register {
+  ::v-deep {
+    .el-input.is-active .el-input__inner,
+    .el-input__inner:focus {
+      border-color: #4093B5;
+    }
+  }
+}
+
+.verifyCode {
+  .send {
+    cursor: pointer;
+    white-space: nowrap;
+    color: #4093B5;
+  }
+}
+</style>

+ 271 - 0
pages/_template/hyt/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-hyt',
+  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(~assets/theme-images/common/pc-link-register-bg.png)
+      no-repeat center;
+
+    .link-register-section {
+      width: 1200px;
+      height: 530px;
+      background: url(~assets/theme-images/common/pc-link-register-section-bg.png)
+        no-repeat center;
+      .content {
+        width: 1030px;
+        height: 400px;
+        background: #fff;
+        background-image: url(~assets/theme-images/common/pc-icon-link-register.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(~assets/theme-images/common/h5-link-register-bg.png)
+      no-repeat center;
+
+    .link-register-section {
+      width: 100vw;
+      height: 119.5vw;
+      background: url(~assets/theme-images/common/h5-link-register-section-bg.png)
+        no-repeat center;
+      .content {
+        width: 88.6vw;
+        height: 106.6vw;
+        background: #fff;
+        background-image: url(~assets/theme-images/common/h5-link-register-section-bg.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>

+ 564 - 0
pages/_template/hyt/index.vue

@@ -0,0 +1,564 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="loadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="fetchClubList"
+    >
+      <div class="page-top flex flex-col justify-center items-center"></div>
+      <div class="page-content">
+        <div
+          class="navbar flex items-center flex-col"
+          :style="{ top: offsetTop }"
+        >
+          <nuxt-link
+            :to="routePrefix + '/approve/device'"
+            class="link flex items-center flex-col"
+          >
+            <span class="icon icon-device"></span>
+            <span class="text">设备认证</span>
+          </nuxt-link>
+          <nuxt-link
+            :to="routePrefix + '/approve/personnel/operate'"
+            class="link flex items-center flex-col md:mt-6 mt-4"
+          >
+            <span class="icon icon-doctor"></span>
+            <span class="text">体疗师认证</span>
+          </nuxt-link>
+        </div>
+
+        <div class="filter">
+          <div class="search">
+            <el-input
+              placeholder="搜索机构"
+              v-model="listQuery.authParty"
+              @change="filterClubList"
+            >
+              <i slot="prefix" class="el-input__icon el-icon-search"></i>
+            </el-input>
+          </div>
+          <div class="area">
+            <RossSelectGroup @change="onCityChange" ref="citySelect" />
+          </div>
+        </div>
+
+        <!-- 明星机构 -->
+        <template v-if="starList.length > 0">
+          <div class="title flex justify-between px-4 pt-8 pb-6 md:px-0">
+            <div>明星机构</div>
+            <nuxt-link :to="routePrefix + '/approve/club/star-list'"
+              >更多<i class="el-icon-arrow-right"></i
+            ></nuxt-link>
+          </div>
+          <!-- 列表 -->
+          <div class="list">
+            <template v-for="item in starList">
+              <div
+                class="section flex justify-between mb-4"
+                :key="item.authId"
+                @click="toDetail(item)"
+              >
+                <img
+                  class="cover"
+                  :src="item.logo || drawLogo(item.authParty)"
+                />
+                <div class="info">
+                  <div class="name" v-text="item.authParty"></div>
+                  <div class="mobile">{{ item.mobile || '暂无' }}</div>
+                  <div class="address">
+                    {{ formatAddress(item.area, item.address) }}
+                  </div>
+                </div>
+              </div>
+            </template>
+          </div>
+        </template>
+
+        <!-- 标题 -->
+        <div class="title flex justify-between px-4 pt-8 pb-6 md:px-0">
+          <div>距您最近...</div>
+          <div>共<span v-text="total" class="font-bold"></span>家授权机构</div>
+        </div>
+        <!-- 列表 -->
+        <div class="list">
+          <template v-for="item in list">
+            <div
+              class="section flex justify-between mb-4"
+              :key="item.authId"
+              @click="toDetail(item)"
+            >
+              <img class="cover" :src="item.logo || drawLogo(item.authParty)" />
+              <div class="info">
+                <div class="name" v-text="item.authParty"></div>
+                <div class="mobile">{{ item.mobile || '暂无' }}</div>
+                <div class="address">
+                  {{ formatAddress(item.area, item.address) }}
+                </div>
+                <div
+                  class="distance"
+                  v-text="formatDistance(item.distance)"
+                  v-if="item.distance && item.distance !== 99999"
+                ></div>
+              </div>
+            </div>
+          </template>
+        </div>
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          name="icon-empty-club.png"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import clubListMixin from '@/mixins/clubList'
+import { objectCover } from '@/utils'
+export default {
+  layout: 'app-hyt',
+  mixins: [clubListMixin],
+  data() {
+    return {
+      scrollTop: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['screenWidth', 'isPc']),
+    offsetTop() {
+      if (this.scrollTop <= window.innerHeight / 2) return '240px'
+      return 240 + this.scrollTop - window.innerHeight / 2 + 'px'
+    },
+  },
+  mounted() {
+    window.addEventListener('scroll', () => {
+      this.scrollTop = document.documentElement.scrollTop
+    })
+  },
+  beforeDestroy() {
+    window.removeEventListener('scroll', () => {})
+  },
+  methods: {
+    // 从缓存中获取数据
+    initFromCache(cacheData) {
+      const data = objectCover(this, cacheData)
+      console.log(data)
+      this.$nextTick(() => {
+        this.$refs.citySelect.initSelectValue({
+          provinceId: data.provinceId,
+          cityId: data.cityId,
+          townId: data.townId,
+        })
+      })
+    },
+
+    // 城市变化
+    onCityChange(valueMap) {
+      const { provinceId, cityId, townId } = valueMap
+      this.listQuery.provinceId = provinceId
+      this.listQuery.cityId = cityId
+      this.listQuery.townId = townId
+      this.filterClubList()
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.el-input {
+  ::v-deep {
+    & > {
+      .el-input.is-active .el-input__inner,
+      .el-input__inner:focus {
+        border-color: #4093B5;
+      }
+    }
+  }
+}
+
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+  }
+  .page-top {
+    height: 530px;
+    background-image: url(~assets/theme-images/hyt/pc/banner-club.jpg);
+    background-size: cover;
+    background-position: center;
+  }
+  .page-content {
+    position: relative;
+    width: 1000px;
+    margin: 0 auto;
+
+    .title {
+      font-size: 16px;
+      color: #404040;
+
+      span {
+        color: #4093B5;
+      }
+    }
+
+    .filter {
+      padding: 48px 0 105px;
+      .search {
+        width: 640px;
+        margin: 0 auto;
+        .el-input {
+          height: 46px;
+          font-size: 16px;
+          .el-input__icon {
+            font-size: 24px;
+            line-height: 46px;
+            margin-left: 12px;
+          }
+
+          ::v-deep {
+            & > .el-input__inner {
+              height: 46px;
+              padding-left: 55px;
+            }
+          }
+        }
+      }
+    }
+
+    .navbar {
+      position: absolute;
+      top: 240px;
+      right: -168px;
+      width: 120px;
+      border-radius: 16px;
+      background: #fff;
+      box-shadow: 0px 6px 20px rgba(40, 40, 40, 0.1);
+      padding: 24px 0;
+      box-sizing: border-box;
+      z-index: 2;
+      .link {
+        &:hover {
+          .icon {
+            &.icon-device {
+              background: url(~assets/theme-images/hyt/pc/nav-entry-device-active.png)
+                  no-repeat center center,
+                linear-gradient(180deg, #4BB9E6 0%, #4093B5 100%);
+              background-size: 48px, 100%;
+            }
+            &.icon-doctor {
+              background: url(~assets/theme-images/hyt/pc/nav-entry-doctor-active.png)
+                  no-repeat center center,
+                linear-gradient(180deg, #4BB9E6 0%, #4093B5 100%);
+              background-size: 48px, 100%;
+            }
+          }
+          .text {
+            color: #4093B5;
+          }
+        }
+
+        span {
+          display: block;
+        }
+
+        .icon {
+          width: 72px;
+          height: 72px;
+          background: linear-gradient(180deg, #f6f6f7 0%, #f6f6f7 100%);
+          border-radius: 12px;
+          transition: all 0.2s;
+
+          &.icon-device {
+            background: url(~assets/theme-images/hyt/pc/nav-entry-device.png)
+              no-repeat center center #f6f6f7;
+            background-size: 48px;
+          }
+
+          &.icon-doctor {
+            background: url(~assets/theme-images/hyt/pc/nav-entry-doctor.png)
+              no-repeat center center #f6f6f7;
+            background-size: 48px;
+          }
+        }
+
+        .text {
+          font-size: 16px;
+          color: #404040;
+          margin-top: 8px;
+        }
+      }
+    }
+
+    .list {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      flex-wrap: wrap;
+
+      .empty {
+        width: 390px;
+      }
+
+      .section {
+        width: 490px;
+        height: 136px;
+        background-color: #f3f5f6;
+        border-radius: 4px;
+        box-sizing: border-box;
+        padding: 16px;
+        cursor: pointer;
+        transition: all 0.4s;
+        &:hover {
+          box-shadow: 0 0 24px rgba(0, 0, 0, 0.2);
+        }
+
+        .cover {
+          display: block;
+          width: 104px;
+          height: 104px;
+        }
+        .info {
+          position: relative;
+          margin-left: 12px;
+          width: 330px;
+          .name {
+            width: 200px;
+            font-size: 18px;
+            color: #101010;
+            font-weight: bold;
+            margin-bottom: 24px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+          }
+          .mobile,
+          .address {
+            width: 268px;
+            position: relative;
+            font-size: 14px;
+            color: #404040;
+            padding-left: 24px;
+            line-height: 24px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            margin-top: 6px;
+            overflow: hidden;
+            &::after {
+              content: '';
+              display: block;
+              width: 16px;
+              height: 16px;
+              position: absolute;
+              left: 0;
+              top: 50%;
+              transform: translateY(-50%);
+              background-size: 16px;
+              background-repeat: no-repeat;
+            }
+          }
+          .mobile {
+            &::after {
+              background-image: url(~assets/theme-images/common/pc-icon-mobile.png);
+            }
+          }
+          .address {
+            &::after {
+              background-image: url(~assets/theme-images/common/pc-icon-address.png);
+            }
+          }
+
+          .distance {
+            position: absolute;
+            font-size: 14px;
+            color: #404040;
+            top: 2px;
+            right: 0;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 100vw;
+    background-image: url(~assets/theme-images/hyt/h5/banner-club.png);
+    background-size: 100vw 100vw !important;
+    background-position: center;
+
+    .logo {
+      display: block;
+      width: 14.8vw;
+      height: 14.8vw;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 4vw;
+      color: #fff;
+    }
+  }
+  .page-content {
+    position: relative;
+
+    .title {
+      font-size: 3.4vw;
+      color: #404040;
+
+      span {
+        color: #4093B5;
+      }
+    }
+
+    .filter {
+      padding: 6.4vw 3.2vw 12.8vw;
+    }
+
+    .navbar {
+      position: fixed;
+      top: 50% !important;
+      right: 3.2vw;
+      left: unset !important;
+      width: 14vw;
+      border-radius: 1.6vw;
+      background: #fff;
+      box-shadow: 0px 0.6vw 2vw rgba(40, 40, 40, 0.1);
+      padding: 2.8vw 0;
+      box-sizing: border-box;
+      z-index: 2;
+      span {
+        display: block;
+      }
+
+      .icon {
+        position: relative;
+        width: 7.2vw;
+        height: 7.2vw;
+        border-radius: 1.2vw;
+        background: linear-gradient(180deg, #4BB9E6 0%, #4093B5 100%);
+
+        &.icon-device,
+        &.icon-doctor {
+          &::after {
+            content: '';
+            display: block;
+            width: 4.8vw;
+            height: 4.8vw;
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            background-size: 4.8vw !important;
+          }
+        }
+
+        &.icon-device {
+          &::after {
+            background: url(~assets/theme-images/hyt/pc/nav-entry-device-active.png)
+              no-repeat center;
+          }
+        }
+
+        &.icon-doctor {
+          &::after {
+            background: url(~assets/theme-images/hyt/pc/nav-entry-doctor-active.png)
+              no-repeat center;
+          }
+        }
+      }
+
+      .text {
+        font-size: 2.4vw;
+        color: #4093B5;
+        margin-top: 1.2vw;
+      }
+    }
+  }
+
+  .list {
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+
+    .section {
+      width: 93.6vw;
+      height: 26vw;
+      background-color: #f3f5f6;
+      border-radius: 4px;
+      box-sizing: border-box;
+      padding: 3.2vw;
+
+      .cover {
+        display: block;
+        width: 19.6vw;
+        height: 19.6vw;
+      }
+      .info {
+        position: relative;
+        margin-left: 3.2vw;
+        .name {
+          width: 48vw;
+          font-size: 4vw;
+          color: #101010;
+          font-weight: bold;
+          margin-bottom: 4vw;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          overflow: hidden;
+        }
+        .mobile,
+        .address {
+          width: 66vw;
+          position: relative;
+          font-size: 3vw;
+          color: #404040;
+          padding-left: 5vw;
+          line-height: 5vw;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          overflow: hidden;
+          &::after {
+            content: '';
+            display: block;
+            width: 4vw;
+            height: 4vw;
+            position: absolute;
+            left: 0;
+            top: 50%;
+            transform: translateY(-50%);
+            background-size: 4vw 4vw;
+            background-repeat: no-repeat;
+          }
+        }
+        .mobile {
+          &::after {
+            background-image: url(~assets/theme-images/common/h5-icon-mobile.png);
+          }
+        }
+        .address {
+          &::after {
+            background-image: url(~assets/theme-images/common/h5-icon-address.png);
+          }
+        }
+
+        .distance {
+          position: absolute;
+          font-size: 3vw;
+          color: #404040;
+          top: 0.8vw;
+          right: 0;
+        }
+      }
+    }
+  }
+}
+</style>

+ 398 - 0
pages/_template/hyt/record/club/detail.vue

@@ -0,0 +1,398 @@
+<template>
+  <div class="page">
+    <div class="page-top"></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.linkMan }}</div>
+        </div>
+        <div class="row">
+          <div class="col">运营联系人手机号:</div>
+          <div class="col">{{ clubInfo.linkMobile }}</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-empty-club.png" description="暂无机构认证~" />
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+import SimpleEmpty from '@/components/SimpleEmpty'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-hyt',
+  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;
+    background: url(~assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 360px;
+    background-position: center;
+
+    .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: #4093B5 !important;
+        }
+        .warning {
+          color: #1890ff !important;
+        }
+        .danger {
+          color: #f94b4b !important;
+        }
+      }
+
+      .col {
+        &:first-child {
+          white-space: nowrap;
+          width: 170px;
+          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 {
+          border: 1px solid #4093B5;
+          color: #4093B5;
+        }
+
+        &.search {
+          background-color: #4093B5;
+          color: #fff;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    background: url(~assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .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: #4093B5 !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 {
+          border: 1px solid #4093B5;
+          color: #4093B5;
+        }
+
+        &.search {
+          background-color: #4093B5;
+          color: #fff;
+        }
+      }
+    }
+  }
+}
+</style>

+ 272 - 0
pages/_template/hyt/record/club/edit.vue

@@ -0,0 +1,272 @@
+<template>
+  <div class="club-info page">
+    <div class="page-top"></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-hyt',
+  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
+        console.log(this.formData)
+        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) {
+      this.formData = data
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+  .page-top {
+    height: 360px;
+    background: url(~assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 360px;
+    background-position: center;
+
+    .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 {
+          background-color: #4093B5;
+          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(~assets/theme-images/common/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;
+    background: url(~assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .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 {
+          background-color: #4093B5;
+          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(~assets/theme-images/common/icon-position.png)
+            no-repeat center;
+          background-size: 3.58vw;
+        }
+      }
+    }
+  }
+}
+</style>

+ 393 - 0
pages/_template/hyt/record/device/detail.vue

@@ -0,0 +1,393 @@
+<template>
+  <div class="page">
+    <div class="page-top"></div>
+    <div class="page-content">
+      <div class="page-title">设备认证</div>
+      <div class="row">
+        <div class="col">认证方式:</div>
+        <div class="col">新设备认证</div>
+      </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.snCode }}</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-hyt',
+  data() {
+    return {
+      relationId: '',
+      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.relationId = this.$route.query.relationId
+      this.getProductDetails()
+    },
+    // 获取认证机构信息
+    async getProductDetails() {
+      try {
+        const res = await this.$http.api.getProductDetails({
+          productId: this.productId,
+          relationId: this.relationId,
+        })
+        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}&relationId=${this.relationId}`
+      )
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@media screen and (min-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 360px;
+    background: url(~/assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 360px;
+    background-position: center;
+
+    .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 {
+      width: 100%;
+      margin-top: -10px;
+      .param {
+        display: table-row;
+        width: 100%;
+      }
+
+      .param-name,
+      .param-content {
+        display: table-cell;
+        font-size: 16px;
+        padding: 12px 0;
+
+        &:nth-child(2n-1) {
+          color: #999999;
+          white-space: nowrap;
+        }
+
+        &:nth-child(2n) {
+          color: #282828;
+          padding-left: 12px;
+        }
+
+        &:nth-child(3) {
+          padding-left: 100px;
+        }
+      }
+    }
+
+    // .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: #4093b5 !important;
+        }
+        &.warning {
+          color: #1890ff !important;
+        }
+        &.danger {
+          color: #f94b4b !important;
+        }
+        &:first-child {
+          width: 100px;
+          color: #666;
+          text-align: right;
+          flex-shrink: 0;
+        }
+
+        &: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 {
+          border: 1px solid #4093b5;
+          color: #4093b5;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    background: url(~assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .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: #4093b5 !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 {
+          border: 1px solid #4093b5;
+          color: #4093b5;
+        }
+      }
+    }
+  }
+}
+</style>

+ 256 - 0
pages/_template/hyt/record/device/edit.vue

@@ -0,0 +1,256 @@
+<template>
+  <div class="club-device page">
+    <div class="page-top"></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-hyt',
+  components: {
+    FormClubDevice,
+  },
+  data() {
+    return {
+      productInfo: {},
+      formData: {},
+      productId: 0,
+      relationId: '',
+      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
+        this.relationId = this.$route.query.relationId
+        const res = await this.$http.api.getProductDetails({
+          productId: this.productId,
+          relationId: this.relationId,
+        })
+        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;
+    background: url(~/assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 360px;
+    background-position: center;
+
+    .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 {
+          background-color: #4093B5;
+          color: #fff;
+        }
+      }
+    }
+
+    .device-param-list {
+      position: relative;
+      .add-param {
+        position: absolute;
+        cursor: pointer;
+        top: -40px;
+        right: 0;
+        text-decoration: underline;
+        font-size: 14px;
+        color: #4093B5;
+      }
+
+      .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;
+    background: url(~assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .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 {
+          background-color: #4093B5;
+          color: #fff;
+        }
+      }
+    }
+
+    .device-param-list {
+      position: relative;
+      .add-param {
+        position: absolute;
+        cursor: pointer;
+        top: -40px;
+        right: 0;
+        font-size: 3.4vw;
+        color: #4093B5;
+      }
+
+      .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>

+ 315 - 0
pages/_template/hyt/record/device/index.vue

@@ -0,0 +1,315 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top"></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-empty-device.png" description="暂无设备~" />
+        </template>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import SimpleEmpty from '@/components/SimpleEmpty'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-hyt',
+  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}&relationId=${item.relationId}`
+      )
+    },
+    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;
+    background: url(~/assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 360px;
+    background-position: center;
+
+    .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: #4093B5;
+            }
+          }
+          &.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(~assets/theme-images/common/pc-icon-detail-more.png)
+            no-repeat center;
+          background-size: 18px;
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page {
+    background: #fff;
+  }
+
+  .page-top {
+    height: 46vw;
+    background: url(~assets/theme-images/hyt/pc/banner-record.png);
+    background-size: auto 46vw;
+    background-position: center;
+
+    .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: #4093B5;
+            }
+          }
+          &.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(~assets/theme-images/common/h5-icon-detail-more.png)
+            no-repeat center;
+          background-size: 18px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 112 - 0
pages/_template/hyt/record/message.vue

@@ -0,0 +1,112 @@
+<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-hyt',
+  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(~assets/theme-images/common/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;
+      background: #4093B5;
+    }
+  }
+}
+
+@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(~assets/theme-images/common/h5-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;
+      background: #4093B5;
+    }
+  }
+}
+</style>

+ 1 - 1
pages/_template/ph/feedback/index.vue

@@ -16,7 +16,7 @@
     <van-dialog v-model="showModal" class="dialog" @confirm="onConfirm">
       <div class="dialog-content">
         <div class="image-icon"></div>
-        <div class="title">提成功</div>
+        <div class="title">提成功</div>
         <div class="tip">您的反馈信息已提交,感谢您的宝贵意见。</div>
         <div class="line" />
       </div>

+ 1 - 1
pages/_template/ross/feedback/index.vue

@@ -17,7 +17,7 @@
     <van-dialog v-model="showModal" class="dialog" @confirm="onConfirm">
       <div class="dialog-content">
         <div class="image-icon"></div>
-        <div class="title">提成功</div>
+        <div class="title">提成功</div>
         <div class="tip">您的反馈信息已提交,感谢您的宝贵意见。</div>
         <div class="line" />
       </div>