yuwenjun1997 пре 2 година
родитељ
комит
3f8a63b473
90 измењених фајлова са 10135 додато и 64 уклоњено
  1. BIN
      assets/theme-images/common/h5-icon-collapse-close.png
  2. BIN
      assets/theme-images/common/h5-icon-collapse.png
  3. BIN
      assets/theme-images/ross/h5-banner-club.jpg
  4. BIN
      assets/theme-images/ross/h5-banner-device.png
  5. BIN
      assets/theme-images/ross/h5-banner-doc.png
  6. BIN
      assets/theme-images/ross/h5-banner-doctor.png
  7. BIN
      assets/theme-images/ross/h5-banner-feedback.png
  8. BIN
      assets/theme-images/ross/h5-banner-record.png
  9. BIN
      assets/theme-images/ross/h5-banner-register.png
  10. BIN
      assets/theme-images/ross/h5-link-entry-doc-active.png
  11. BIN
      assets/theme-images/ross/h5-link-entry-feedback-active.png
  12. BIN
      assets/theme-images/ross/h5-link-entry-register-active.png
  13. BIN
      assets/theme-images/ross/h5-nav-entry-device-active.png
  14. BIN
      assets/theme-images/ross/h5-nav-entry-doctor-active.png
  15. BIN
      assets/theme-images/ross/pc-banner-club.jpg
  16. BIN
      assets/theme-images/ross/pc-banner-device.png
  17. BIN
      assets/theme-images/ross/pc-banner-doc.png
  18. BIN
      assets/theme-images/ross/pc-banner-doctor.png
  19. BIN
      assets/theme-images/ross/pc-banner-feedback.png
  20. BIN
      assets/theme-images/ross/pc-banner-record.png
  21. BIN
      assets/theme-images/ross/pc-banner-register.png
  22. BIN
      assets/theme-images/ross/pc-link-entry-doc-active.png
  23. BIN
      assets/theme-images/ross/pc-link-entry-doc.png
  24. BIN
      assets/theme-images/ross/pc-link-entry-feedback-active.png
  25. BIN
      assets/theme-images/ross/pc-link-entry-feedback.png
  26. BIN
      assets/theme-images/ross/pc-link-entry-register-active.png
  27. BIN
      assets/theme-images/ross/pc-link-entry-register.png
  28. BIN
      assets/theme-images/ross/pc-nav-entry-device-active.png
  29. BIN
      assets/theme-images/ross/pc-nav-entry-device.png
  30. BIN
      assets/theme-images/ross/pc-nav-entry-doctor-active.png
  31. BIN
      assets/theme-images/ross/pc-nav-entry-doctor.png
  32. 4 4
      assets/themes/variables/ross.scss
  33. 288 0
      components/RossSelectGroup/index.vue
  34. 4 4
      components/SimpleLogin/index.vue
  35. 254 0
      components/SimpleProgress/index.vue
  36. 4 0
      configs/mode-map.js
  37. 524 0
      layouts/app-ross.vue
  38. 3 3
      layouts/app.vue
  39. 1 1
      pages/_template/app/approve/club/detail.vue
  40. 1 1
      pages/_template/app/approve/club/index.vue
  41. 1 1
      pages/_template/app/approve/device/index.vue
  42. 1 1
      pages/_template/app/approve/device/list.vue
  43. 1 1
      pages/_template/app/approve/index.vue
  44. 1 1
      pages/_template/app/approve/personnel/operate/detail.vue
  45. 1 1
      pages/_template/app/approve/personnel/operate/index.vue
  46. 22 1
      pages/_template/app/database/article-detail.vue
  47. 6 8
      pages/_template/app/database/file.vue
  48. 6 9
      pages/_template/app/database/image.vue
  49. 5 7
      pages/_template/app/database/package.vue
  50. 5 7
      pages/_template/app/database/video.vue
  51. 1 1
      pages/_template/app/feedback/index.vue
  52. 1 1
      pages/_template/app/form/club-register.vue
  53. 1 1
      pages/_template/app/form/link-register.vue
  54. 1 1
      pages/_template/app/index.vue
  55. 1 1
      pages/_template/app/record/club/detail.vue
  56. 1 1
      pages/_template/app/record/club/edit.vue
  57. 1 1
      pages/_template/app/record/device/detail.vue
  58. 1 1
      pages/_template/app/record/device/edit.vue
  59. 1 1
      pages/_template/app/record/device/index.vue
  60. 1 1
      pages/_template/app/record/message.vue
  61. 4 4
      pages/_template/ldm/database/package.vue
  62. 509 0
      pages/_template/ross/approve/club/detail.vue
  63. 273 0
      pages/_template/ross/approve/device/index.vue
  64. 324 0
      pages/_template/ross/approve/device/list.vue
  65. 227 0
      pages/_template/ross/approve/device/search.vue
  66. 333 0
      pages/_template/ross/approve/personnel/operate/detail.vue
  67. 350 0
      pages/_template/ross/approve/personnel/operate/index.vue
  68. 116 0
      pages/_template/ross/database/article-detail.vue
  69. 303 0
      pages/_template/ross/database/article.vue
  70. 334 0
      pages/_template/ross/database/file.vue
  71. 348 0
      pages/_template/ross/database/image.vue
  72. 302 0
      pages/_template/ross/database/package.vue
  73. 361 0
      pages/_template/ross/database/video.vue
  74. 264 0
      pages/_template/ross/feedback/index.vue
  75. 531 0
      pages/_template/ross/form/club-register.vue
  76. 603 0
      pages/_template/ross/form/components/form-club-device.vue
  77. 829 0
      pages/_template/ross/form/components/form-club-info.vue
  78. 230 0
      pages/_template/ross/form/components/form-club-register.vue
  79. 271 0
      pages/_template/ross/form/link-register.vue
  80. 649 0
      pages/_template/ross/index.vue
  81. 399 0
      pages/_template/ross/record/club/detail.vue
  82. 279 0
      pages/_template/ross/record/club/edit.vue
  83. 362 0
      pages/_template/ross/record/device/detail.vue
  84. 264 0
      pages/_template/ross/record/device/edit.vue
  85. 321 0
      pages/_template/ross/record/device/index.vue
  86. 116 0
      pages/_template/ross/record/message.vue
  87. 2 0
      store/app.js
  88. 1 0
      store/getters.js
  89. 61 0
      utils/donwload-tools.js
  90. 27 0
      utils/index.js

BIN
assets/theme-images/common/h5-icon-collapse-close.png


BIN
assets/theme-images/common/h5-icon-collapse.png


BIN
assets/theme-images/ross/h5-banner-club.jpg


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 4 - 4
assets/themes/variables/ross.scss

@@ -6,7 +6,7 @@ $rossTuple: (
   // pc端
   pc-banner-home: url(~/assets/theme-images/ross/pc-banner-home.png),
   pc-banner-approve: url(~/assets/theme-images/ross/pc-banner-approve.png),
-  pc-banner-club: url(~/assets/theme-images/ross/pc-banner-club.png),
+  pc-banner-club: url(~/assets/theme-images/ross/pc-banner-club.jpg),
   pc-banner-device: url(~/assets/theme-images/ross/pc-banner-device.png),
   pc-banner-doctor: url(~/assets/theme-images/ross/pc-banner-doctor.png),
   pc-banner-feedback: url(~/assets/theme-images/ross/pc-banner-feedback.png),
@@ -30,14 +30,14 @@ $rossTuple: (
   //  h5 端
   h5-banner-home: url(~/assets/theme-images/ross/h5-banner-home.png),
   h5-banner-approve: url(~/assets/theme-images/ross/h5-banner-approve.png),
-  h5-banner-club: url(~/assets/theme-images/ross/h5-banner-club.png),
+  h5-banner-club: url(~/assets/theme-images/ross/h5-banner-club.jpg),
   h5-banner-device: url(~/assets/theme-images/ross/h5-banner-device.png),
   h5-banner-doctor: url(~/assets/theme-images/ross/h5-banner-doctor.png),
   h5-banner-feedback: url(~/assets/theme-images/ross/h5-banner-feedback.png),
   h5-banner-doc: url(~/assets/theme-images/ross/h5-banner-doc.png),
   h5-banner-register: url(~/assets/theme-images/ross/h5-banner-register.png),
-  h5-banner-record-club: url(~/assets/theme-images/ross/pc-banner-record.png),
-  h5-banner-record-device: url(~/assets/theme-images/ross/pc-banner-record.png),
+  h5-banner-record-club: url(~/assets/theme-images/ross/h5-banner-record.png),
+  h5-banner-record-device: url(~/assets/theme-images/ross/h5-banner-record.png),
   h5-entry-club-bg: url(~/assets/theme-images/ross/h5-entry-club-bg.png),
   h5-entry-device-bg: url(~/assets/theme-images/ross/h5-entry-device-bg.png),
   h5-entry-doctor-bg: url(~/assets/theme-images/ross/h5-entry-doctor-bg.png),

+ 288 - 0
components/RossSelectGroup/index.vue

@@ -0,0 +1,288 @@
+<template>
+  <div class="select-group">
+    <div class="select-group__item" @click.stop="toggleMenu('province')">
+      <div class="info select-click-type">
+        <span>{{ province ? province.name : '省份' }}</span>
+        <span
+          class="el-icon-circle-close"
+          v-if="province"
+          @click.stop="onClear('province', $event)"
+        ></span>
+        <span class="icon el-icon-caret-bottom" v-else></span>
+      </div>
+      <el-select
+        ref="provinceSelect"
+        v-model="provinceId"
+        @change="onProvinceChange"
+        :clearable="true"
+      >
+        <template v-for="item in provinceList">
+          <el-option :label="item.name" :value="item.id" :key="item.id">
+          </el-option>
+        </template>
+      </el-select>
+    </div>
+    <div class="select-group__item" @click.stop="toggleMenu('city')">
+      <div class="info select-click-type">
+        <span>{{ city ? city.name : '城市' }}</span>
+        <span
+          class="el-icon-circle-close"
+          v-if="city"
+          @click.stop="onClear('city', $event)"
+        ></span>
+        <span class="icon el-icon-caret-bottom" v-else></span>
+      </div>
+      <el-select
+        ref="citySelect"
+        v-model="cityId"
+        @change="onCityChange"
+        :clearable="true"
+      >
+        <template v-for="item in cityList">
+          <el-option :label="item.name" :value="item.id" :key="item.id">
+          </el-option>
+        </template>
+      </el-select>
+    </div>
+    <div class="select-group__item" @click.stop="toggleMenu('town')">
+      <div class="info select-click-type">
+        <span>{{ town ? town.name : '县区' }}</span>
+        <span
+          class="el-icon-circle-close"
+          v-if="town"
+          @click.stop="onClear('town', $event)"
+        ></span>
+        <span class="icon el-icon-caret-bottom" v-else></span>
+      </div>
+      <el-select
+        ref="townSelect"
+        v-model="townId"
+        @change="onTownChange"
+        :clearable="true"
+      >
+        <template v-for="item in townList">
+          <el-option :label="item.name" :value="item.id" :key="item.id">
+          </el-option>
+        </template>
+      </el-select>
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  data() {
+    return {
+      provinceId: '',
+      cityId: '',
+      townId: '',
+      provinceList: [],
+      province: null,
+      city: null,
+      town: null,
+      visibleMap: {
+        province: false,
+        city: false,
+        town: false,
+      },
+    }
+  },
+  computed: {
+    cityList() {
+      return this.province ? this.province.children : []
+    },
+    townList() {
+      return this.city ? this.city.children : []
+    },
+  },
+  mounted() {
+    window.addEventListener('click', () => {
+      this.visibleMap = {
+        province: false,
+        city: false,
+        town: false,
+      }
+    })
+  },
+  beforeDestroy() {
+    window.removeEventListener('click', () => {})
+  },
+  created() {
+    this.initSelectValue()
+  },
+  methods: {
+    async initSelectValue(data) {
+      try {
+        const res = await this.$http.api.fetchAllCityList()
+        this.provinceList = res.data
+        if (data) {
+          this.provinceId = data.provinceId
+          this.cityId = data.cityId
+          this.townId = data.townId
+          if (this.provinceId) {
+            this.province = this.provinceList.find(
+              (item) => item.id === this.provinceId
+            )
+          }
+          if (this.cityId) {
+            this.city = this.cityList.find((item) => item.id === this.cityId)
+          }
+          if (this.townId) {
+            this.town = this.townList.find((item) => item.id === this.townId)
+          }
+        }
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    resetVisiable(type, flag) {
+      for (const key in this.visibleMap) {
+        if (type === key) {
+          this.visibleMap[key] = flag
+        } else {
+          this.visibleMap[key] = false
+        }
+      }
+    },
+
+    toggleMenu(type) {
+      const action = {
+        province: this.$refs.provinceSelect,
+        city: this.$refs.citySelect,
+        town: this.$refs.townSelect,
+      }
+      const target = action[type]
+      if (!this.visibleMap[type]) {
+        target.visible = true
+        this.resetVisiable(type, true)
+      } else {
+        target.visible = false
+        this.resetVisiable(type, false)
+      }
+    },
+    onClear(type, $event) {
+      const action = {
+        province: this.$refs.provinceSelect.handleClearClick,
+        city: this.$refs.citySelect.handleClearClick,
+        town: this.$refs.townSelect.handleClearClick,
+      }
+      action[type]($event)
+    },
+    onProvinceChange(value) {
+      console.log(this.$refs)
+      this.cityId = ''
+      this.townId = ''
+      this.city = null
+      this.town = null
+      this.province = this.provinceList.find((item) => item.id === value)
+      this.onEmit()
+    },
+    onCityChange(value) {
+      this.townId = ''
+      this.town = null
+      this.city = this.cityList.find((item) => item.id === value)
+      this.onEmit()
+    },
+    onTownChange(value) {
+      this.town = this.townList.find((item) => item.id === value)
+      this.onEmit()
+    },
+    onEmit() {
+      this.$emit('change', {
+        provinceId: this.provinceId,
+        cityId: this.cityId,
+        townId: this.townId,
+      })
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+// pc 端
+@media screen and (min-width: 768px) {
+  .select-group {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-top: 12px;
+    color: #282828;
+
+    .select-group__item {
+      cursor: pointer;
+      position: relative;
+      width: 160px;
+      text-align: center;
+
+      .info {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 160px;
+        z-index: 1;
+        line-height: 40px;
+      }
+
+      .icon {
+        color: #999;
+      }
+
+      .el-icon-circle-close {
+        &:hover {
+          @include themify($themes) {
+            color: themed('color');
+          }
+        }
+      }
+
+      .el-select {
+        position: absolute;
+        left: 30px;
+        top: 0;
+        width: 160px;
+        opacity: 0;
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .select-group {
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+    margin-top: 12px;
+    color: #282828;
+    font-size: 3.4vw;
+
+    .select-group__item {
+      cursor: pointer;
+      position: relative;
+      width: 20vw;
+      text-align: center;
+
+      .info {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 20vw;
+        z-index: 1;
+        line-height: 40px;
+      }
+
+      .icon {
+        color: #999;
+      }
+
+      .el-select {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 20vw;
+        opacity: 0;
+      }
+    }
+  }
+}
+</style>

+ 4 - 4
components/SimpleLogin/index.vue

@@ -232,10 +232,10 @@ export default {
       this.onClose()
       // this.$router.push(this.routePrefix)
 
-      const clubRegisterLink = this.$getStorage(
-        this.routePrefix,
-        'club-register-link'
-      )
+      // const clubRegisterLink = this.$getStorage(
+      //   this.routePrefix,
+      //   'club-register-link'
+      // )
       // if (clubRegisterLink) {
       //   this.$removeStorage(this.routePrefix, 'club-register-link')
       //   this.$setStorage(this.routePrefix, 'bind-flag', true)

+ 254 - 0
components/SimpleProgress/index.vue

@@ -0,0 +1,254 @@
+<template>
+  <div
+    class="el-progress"
+    :class="[
+      'el-progress--' + type,
+      status ? 'is-' + status : '',
+      {
+        'el-progress--without-text': !showText,
+        'el-progress--text-inside': textInside,
+      },
+    ]"
+    role="progressbar"
+    :aria-valuenow="percentage"
+    aria-valuemin="0"
+    aria-valuemax="100"
+  >
+    <div class="el-progress-bar" v-if="type === 'line'">
+      <div
+        class="el-progress-bar__outer"
+        :style="{ height: strokeWidth + 'px' }"
+      >
+        <div class="el-progress-bar__inner" :style="barStyle">
+          <div class="el-progress-bar__innerText" v-if="showText && textInside">
+            {{ content }}
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      class="el-progress-circle"
+      :style="{ height: width + 'px', width: width + 'px' }"
+      v-else
+    >
+      <svg viewBox="0 0 100 100">
+        <path
+          class="el-progress-circle__track"
+          :d="trackPath"
+          stroke="#e5e9f2"
+          :stroke-width="relativeStrokeWidth"
+          fill="none"
+          :style="trailPathStyle"
+        ></path>
+        <path
+          class="el-progress-circle__path"
+          :d="trackPath"
+          :stroke="stroke"
+          fill="none"
+          :stroke-linecap="strokeLinecap"
+          :stroke-width="percentage ? relativeStrokeWidth : 0"
+          :style="circlePathStyle"
+        ></path>
+      </svg>
+    </div>
+    <div
+      class="el-progress__text"
+      v-if="showText && !textInside"
+      :style="{ fontSize: progressTextSize + 'px' }"
+    >
+      <template v-if="!status">{{ content }}</template>
+      <i v-else :class="iconClass"></i>
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  name: 'SimpleProgress',
+  props: {
+    type: {
+      type: String,
+      default: 'line',
+      validator: (val) => ['line', 'circle', 'dashboard'].indexOf(val) > -1,
+    },
+    // percentage: {
+    //   type: Number,
+    //   default: 0,
+    //   required: true,
+    //   validator: (val) => val >= 0 && val <= 100,
+    // },
+    status: {
+      type: String,
+      validator: (val) => ['success', 'exception', 'warning'].indexOf(val) > -1,
+    },
+    strokeWidth: {
+      type: Number,
+      default: 6,
+    },
+    strokeLinecap: {
+      type: String,
+      default: 'round',
+    },
+    textInside: {
+      type: Boolean,
+      default: false,
+    },
+    width: {
+      type: Number,
+      default: 126,
+    },
+    showText: {
+      type: Boolean,
+      default: true,
+    },
+    color: {
+      type: [String, Array, Function],
+      default: '',
+    },
+    format: Function,
+  },
+  data() {
+    return {
+      percentage: 0,
+    }
+  },
+  watch: {
+    percentage(val) {
+      if (val < 0) this.percentage = 0
+      if (val > 100) this.percentage = 100
+    },
+  },
+  computed: {
+    barStyle() {
+      const style = {}
+      style.width = this.percentage + '%'
+      style.backgroundColor = this.getCurrentColor(this.percentage)
+      return style
+    },
+    relativeStrokeWidth() {
+      return ((this.strokeWidth / this.width) * 100).toFixed(1)
+    },
+    radius() {
+      if (this.type === 'circle' || this.type === 'dashboard') {
+        return parseInt(50 - parseFloat(this.relativeStrokeWidth) / 2, 10)
+      } else {
+        return 0
+      }
+    },
+    trackPath() {
+      const radius = this.radius
+      const isDashboard = this.type === 'dashboard'
+      return `
+          M 50 50
+          m 0 ${isDashboard ? '' : '-'}${radius}
+          a ${radius} ${radius} 0 1 1 0 ${isDashboard ? '-' : ''}${radius * 2}
+          a ${radius} ${radius} 0 1 1 0 ${isDashboard ? '' : '-'}${radius * 2}
+          `
+    },
+    perimeter() {
+      return 2 * Math.PI * this.radius
+    },
+    rate() {
+      return this.type === 'dashboard' ? 0.75 : 1
+    },
+    strokeDashoffset() {
+      const offset = (-1 * this.perimeter * (1 - this.rate)) / 2
+      return `${offset}px`
+    },
+    trailPathStyle() {
+      return {
+        strokeDasharray: `${this.perimeter * this.rate}px, ${this.perimeter}px`,
+        strokeDashoffset: this.strokeDashoffset,
+      }
+    },
+    circlePathStyle() {
+      return {
+        strokeDasharray: `${
+          this.perimeter * this.rate * (this.percentage / 100)
+        }px, ${this.perimeter}px`,
+        strokeDashoffset: this.strokeDashoffset,
+        transition: 'stroke-dasharray 0.6s ease 0s, stroke 0.6s ease',
+      }
+    },
+    stroke() {
+      let ret
+      if (this.color) {
+        ret = this.getCurrentColor(this.percentage)
+      } else {
+        switch (this.status) {
+          case 'success':
+            ret = '#13ce66'
+            break
+          case 'exception':
+            ret = '#ff4949'
+            break
+          case 'warning':
+            ret = '#e6a23c'
+            break
+          default:
+            ret = '#20a0ff'
+        }
+      }
+      return ret
+    },
+    iconClass() {
+      if (this.status === 'warning') {
+        return 'el-icon-warning'
+      }
+      if (this.type === 'line') {
+        return this.status === 'success'
+          ? 'el-icon-circle-check'
+          : 'el-icon-circle-close'
+      } else {
+        return this.status === 'success' ? 'el-icon-check' : 'el-icon-close'
+      }
+    },
+    progressTextSize() {
+      return this.type === 'line'
+        ? 12 + this.strokeWidth * 0.4
+        : this.width * 0.111111 + 2
+    },
+    content() {
+      if (typeof this.format === 'function') {
+        return this.format(this.percentage) || ''
+      } else {
+        return `${this.percentage}%`
+      }
+    },
+  },
+  methods: {
+    getCurrentColor(percentage) {
+      if (typeof this.color === 'function') {
+        return this.color(percentage)
+      } else if (typeof this.color === 'string') {
+        return this.color
+      } else {
+        return this.getLevelColor(percentage)
+      }
+    },
+    getLevelColor(percentage) {
+      const colorArray = this.getColorArray().sort(
+        (a, b) => a.percentage - b.percentage
+      )
+      for (let i = 0; i < colorArray.length; i++) {
+        if (colorArray[i].percentage > percentage) {
+          return colorArray[i].color
+        }
+      }
+      return colorArray[colorArray.length - 1].color
+    },
+    getColorArray() {
+      const color = this.color
+      const span = 100 / color.length
+      return color.map((seriesColor, index) => {
+        if (typeof seriesColor === 'string') {
+          return {
+            color: seriesColor,
+            percentage: (index + 1) * span,
+          }
+        }
+        return seriesColor
+      })
+    },
+  },
+}
+</script>

+ 4 - 0
configs/mode-map.js

@@ -4,4 +4,8 @@ export default [
     authUserId: 10,
     mode: 'ldm',
   },
+  {
+    authUserId: 12,
+    mode: 'ross',
+  },
 ]

+ 524 - 0
layouts/app-ross.vue

@@ -0,0 +1,524 @@
+<template>
+  <div :class="themeClass">
+    <div class="layout" v-if="isMounted">
+      <div class="header">
+        <div class="navbar flex justify-between items-center">
+          <div class="logo flex items-center" @click="backHome">
+            <img
+              src="~/assets/theme-images/ross/ross-logo-f.png"
+              v-if="themeName === 'ross'"
+              class="ross"
+            />
+            <img src="~/assets/theme-images/common/icon-logo.png" v-else />
+            <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="toDetail(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">
+                <span v-text="userInfo.mobile"></span>
+                <span class="underline logout" @click="logout">退出登录</span>
+              </template>
+              <template v-else>
+                <div class="flex justify-center">
+                  <div
+                    class="login mx-3 rounded-sm border-white md:leading-6"
+                    @click="onLogin"
+                  >
+                    登录
+                  </div>
+                  |
+                  <div
+                    class="register mx-3 rounded-sm border-white md:leading-6"
+                    @click="onRegister"
+                  >
+                    注册
+                  </div>
+                </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">
+        - 由采美网提供技术支持 -
+      </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="toDetail(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'
+import { isWeChat } from '~/utils/validator'
+import { toAuthorization } from '~/utils'
+export default {
+  computed: {
+    ...mapGetters([
+      'userInfo',
+      'accessToken',
+      'authUserId',
+      'appId',
+      'accountType',
+      'routePrefix',
+      'themeName',
+      'routePrefix',
+      'isPc',
+    ]),
+    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 {
+      isMounted: false,
+      formType: 'login',
+      drawer: false,
+      list: [
+        {
+          id: 1,
+          name: '授权申请',
+          path: '/form/club-register',
+          icon: 'icon-register',
+        },
+        {
+          id: 2,
+          name: '产品资料',
+          path: '/database/article',
+          icon: 'icon-doc',
+        },
+        {
+          id: 3,
+          name: '意见反馈',
+          path: '/feedback',
+          icon: 'icon-feedback',
+        },
+      ],
+    }
+  },
+  mounted() {
+    this.init()
+    console.log(1)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', () => {})
+  },
+  methods: {
+    toDetail(item) {
+      this.drawer = false
+      const hasLogin = this.$store.getters.accessToken
+      // 保存登录重定向路由
+      this.$setStorage(
+        this.routePrefix,
+        'login_redicret',
+        this.routePrefix + item.path
+      )
+      if (item.id > 1 && !hasLogin) {
+        // 在微信浏览器中使用微信授权登录
+        if (isWeChat() && this.appId && this.accountType === 2) {
+          const payload = {
+            authUserId: this.authUserId,
+            routePrefix: this.routePrefix,
+          }
+          return toAuthorization(this.appId, payload)
+        }
+        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
+    },
+
+    init() {
+      this.responseWidth()
+      this.initPageData()
+    },
+    // 初始化数据页面公共数据
+    initPageData() {
+      // 获取供应商id
+      const authUserId = parseInt(this.$route.params.template)
+      const routePrefix = `/${authUserId}/ross`
+      // 保存页面路由前缀
+      this.$store.commit('app/SET_ROUTE_PREFIX', routePrefix)
+      // 保存用户AppId
+      this.$store.commit('user/SET_AUTH_USER_ID', authUserId)
+
+      // 设置页面主题12
+      // if (authUserId === parseInt(12)) {
+      //   this.$store.commit('app/SET_PAGE_THEME', 'ross')
+      // }
+      this.$store.commit('app/SET_PAGE_THEME', 'ross')
+
+      // 获取用户信息
+      let userInfo = this.$getStorage(routePrefix, 'userInfo')
+      if (userInfo && userInfo.authUserId === authUserId) {
+        this.$store.commit('user/SET_USER_INFO', userInfo)
+      }
+      // 初始化供应商信息
+      this.fetchSupplierInfo()
+    },
+
+    // 获取供应商信息
+    async fetchSupplierInfo() {
+      try {
+        const res = await this.$http.api.fetchSupplierInfo({
+          authUserId: this.authUserId,
+        })
+        this.$store.commit('supplier/SET_SUPPLIER_INFO', res.data)
+        this.$store.commit('user/SET_APPID', res.data.appId)
+        // 如果appId存在
+        if (res.data.appId) {
+          this.checkAccountType(res.data.appId)
+        }
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isMounted = true
+        // 清除缓存
+        this.refreshCacheData()
+      }
+    },
+
+    // 校验公众号类型
+    async checkAccountType(appId) {
+      try {
+        // 1订阅号,2服务号
+        const res = await this.$http.api.checkAccountType({ appId })
+        this.$store.commit('user/SET_ACCOUNT_TYPE', res.data)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+
+    onLogin() {
+      // 在微信浏览器中使用微信授权登录
+      if (isWeChat() && this.appId && this.accountType === 2) {
+        const payload = {
+          authUserId: this.authUserId,
+          routePrefix: this.routePrefix,
+        }
+        return toAuthorization(this.appId, payload)
+      }
+      this.formType = 'login'
+      this.$store.commit('app/SHOW_LOGIN')
+    },
+
+    onRegister() {
+      this.formType = 'register'
+      console.log(this.formType)
+      this.$store.commit('app/SHOW_LOGIN')
+    },
+
+    // 退出登录
+    logout() {
+      this.$store.dispatch('user/logout')
+      console.log(this.routePrefix)
+      this.$removeStorage(this.routePrefix, 'userInfo')
+      this.backHome()
+    },
+
+    // 回到首页
+    backHome() {
+      if (this.$route.path === this.routePrefix) return
+      this.$router.replace(this.routePrefix)
+    },
+
+    // 响应页面宽度变化
+    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">
+// 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: 44px;
+          height: 44px;
+
+          &.ross {
+            width: 85px;
+            height: 27px;
+            margin-right: 15px;
+            transform: translateY(-2px);
+          }
+        }
+        span {
+          font-size: 24px;
+          color: #fff;
+        }
+      }
+
+      .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/ross/pc-link-entry-register-active.png);
+              }
+              &.icon-doc {
+                background-image: url(~assets/theme-images/ross/pc-link-entry-doc-active.png);
+              }
+              &.icon-feedback {
+                background-image: url(~assets/theme-images/ross/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/ross/pc-link-entry-register.png);
+          }
+          &.icon-doc {
+            background-image: url(~assets/theme-images/ross/pc-link-entry-doc.png);
+          }
+          &.icon-feedback {
+            background-image: url(~assets/theme-images/ross/pc-link-entry-feedback.png);
+          }
+        }
+        .text {
+          font-size: 16px;
+          color: #fff;
+        }
+      }
+
+      .user-info {
+        color: #fff;
+        font-size: 16px;
+        margin-left: 48px;
+
+        .login,
+        .register,
+        .logout {
+          cursor: pointer;
+
+          &:hover {
+            @include themify($themes) {
+              color: themed('color');
+            }
+          }
+        }
+      }
+    }
+    .content {
+      min-height: calc(100vh - 80px - 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: 8vw;
+          height: 8vw;
+
+          &.ross {
+            width: 12.2vw;
+            height: 3.9vw;
+            margin-right: 1.9vw;
+            transform: translateY(-0.6vw);
+          }
+        }
+        span {
+          font-size: 4vw;
+          color: #fff;
+        }
+      }
+
+      .user-info {
+        color: #fff;
+        font-size: 3vw;
+
+        .logout {
+          margin: 0 1.6vw;
+        }
+      }
+
+      .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);
+    }
+
+    .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/ross/h5-link-entry-register-active.png);
+        }
+        &.icon-doc {
+          background-image: url(~assets/theme-images/ross/h5-link-entry-doc-active.png);
+        }
+        &.icon-feedback {
+          background-image: url(~assets/theme-images/ross/h5-link-entry-feedback-active.png);
+        }
+      }
+      .text {
+        font-size: 3.4vw;
+        color: #282828;
+      }
+    }
+  }
+}
+</style>

+ 3 - 3
layouts/app.vue

@@ -105,9 +105,9 @@ export default {
       this.$store.commit('user/SET_AUTH_USER_ID', authUserId)
 
       // 设置页面主题12
-      if (authUserId === parseInt(12)) {
-        this.$store.commit('app/SET_PAGE_THEME', 'ross')
-      }
+      // if (authUserId === parseInt(12)) {
+      //   this.$store.commit('app/SET_PAGE_THEME', 'ross')
+      // }
 
       // 获取用户信息
       let userInfo = this.$getStorage(routePrefix, 'userInfo')

+ 1 - 1
pages/_template/app/approve/club/detail.vue

@@ -58,7 +58,7 @@ import { drawLogo } from '@/utils'
 import { mapNavigate } from '@/utils/map-utils'
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   filters: {
     formatEmpty(val) {
       return val || '未知'

+ 1 - 1
pages/_template/app/approve/club/index.vue

@@ -79,7 +79,7 @@ import { mapGetters } from 'vuex'
 import { loactionSelf } from '@/utils/map-utils'
 import { drawLogo, debounce } from '@/utils'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   data() {
     return {
       isLoadingMore: true,

+ 1 - 1
pages/_template/app/approve/device/index.vue

@@ -56,7 +56,7 @@
 import { mapGetters } from 'vuex'
 import { debounce } from '@/utils'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   data() {
     return {
       isLoadingMore: true,

+ 1 - 1
pages/_template/app/approve/device/list.vue

@@ -66,7 +66,7 @@
 import { debounce } from '@/utils'
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   filters: {
     formatSnCode(code) {
       if (!code) return ''

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

@@ -24,7 +24,7 @@
 <script>
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   data() {
     return {
       list: [],

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

@@ -49,7 +49,7 @@
 <script>
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   data() {
     return {
       doctorId: '',

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

@@ -62,7 +62,7 @@
 import { mapGetters } from 'vuex'
 import { debounce } from '@/utils'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   data() {
     return {
       isLoadingMore: true,

+ 22 - 1
pages/_template/app/database/article-detail.vue

@@ -71,7 +71,6 @@ export default {
     padding-bottom: 24px;
     border-bottom: 1px solid #d8d8d8;
     .title {
-      @include ellipsis(2);
       font-size: 28px;
       color: #101010;
       line-height: 1.6;
@@ -91,5 +90,27 @@ export default {
 
 // 移动 端
 @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>

+ 6 - 8
pages/_template/app/database/file.vue

@@ -41,7 +41,7 @@
             <div class="info">
               <div class="name" v-text="item.fileName"></div>
               <div class="date">{{ item.createTime | dateFormat }}</div>
-              <div class="download" @click.stop="downloadLink(item, $event)">
+              <div class="download" @click.stop="download(item, $event)">
                 下载
               </div>
             </div>
@@ -59,8 +59,8 @@
 
 <script>
 import { mapGetters } from 'vuex'
-import { tabs } from '@/configs/tabs'
-import downloadLink from '@/utils/download-link'
+import { tabs } from '~/configs/tabs'
+import downloadFile from '~/utils/donwload-tools'
 import { debounce } from '~/utils'
 
 export default {
@@ -91,9 +91,9 @@ export default {
   },
   methods: {
     // 下载方法
-    downloadLink(item, $event) {
+    download(item, $event) {
       const url = `${process.env.BASE_URL}/download/file?ossName=${item.fileDownloadUrl}&fileName=${item.fileName}`
-      downloadLink(url, $event)
+      downloadFile(url, item.fileName, this, $event)
     },
     // 预览文件
     previewFile(item) {
@@ -231,9 +231,7 @@ export default {
           top: 50%;
           transform: translateY(-50%);
           font-size: 16px;
-          @include themify($themes) {
-            color: themed('color');
-          }
+          color: #1890ff;
           cursor: pointer;
 
           &::before {

+ 6 - 9
pages/_template/app/database/image.vue

@@ -36,10 +36,7 @@
             <div class="info">
               <div class="name" v-text="item.imageTitle"></div>
               <div class="date">{{ item.createTime | dateFormat }}</div>
-              <div
-                class="download"
-                @click="downloadLink(item.imageZipUrl, $event)"
-              >
+              <div class="download" @click="download(item.imageZipUrl, $event)">
                 保存所有图片
               </div>
             </div>
@@ -68,7 +65,7 @@
 <script>
 import { mapGetters } from 'vuex'
 import { tabs } from '@/configs/tabs'
-import downloadLink from '@/utils/download-link'
+import downloadFile from '~/utils/donwload-tools'
 import { ImagePreview } from 'vant'
 import { debounce } from '~/utils'
 
@@ -99,7 +96,9 @@ export default {
   },
   methods: {
     // 下载方法
-    downloadLink,
+    download(url, $event) {
+      downloadFile(url, '图片包', this, $event)
+    },
     // 获取列表
     fetchList: debounce(async function () {
       try {
@@ -227,9 +226,7 @@ export default {
           bottom: 0;
           right: 0;
           position: absolute;
-          @include themify($themes) {
-            color: themed('color');
-          }
+          color: #1890ff;
           font-size: 16px;
           cursor: pointer;
         }

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

@@ -39,7 +39,7 @@
           >
             <div class="info">
               <div class="name" v-text="item.fileName"></div>
-              <div class="download" @click="downloadLink(item, $event)">
+              <div class="download" @click="download(item, $event)">
                 点击下载
               </div>
             </div>
@@ -59,7 +59,7 @@
 <script>
 import { mapGetters } from 'vuex'
 import { tabs } from '@/configs/tabs'
-import downloadLink from '@/utils/download-link'
+import downloadFile from '~/utils/donwload-tools'
 import { debounce } from '~/utils'
 
 export default {
@@ -90,9 +90,9 @@ export default {
   },
   methods: {
     // 下载方法
-    downloadLink(item, $event) {
+    download(item, $event) {
       const url = `${process.env.BASE_URL}/download/file?ossName=${item.fileDownloadUrl}&fileName=${item.fileName}`
-      downloadLink(url, $event)
+      downloadFile(url, item.fileName, this, $event)
     },
     // 获取列表
     fetchList: debounce(async function () {
@@ -209,9 +209,7 @@ export default {
           top: 50%;
           transform: translateY(-50%);
           font-size: 16px;
-          @include themify($themes) {
-            color: themed('color');
-          }
+          color: #1890ff;
           cursor: pointer;
           &::after {
             content: '>';

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

@@ -40,7 +40,7 @@
             <div class="info">
               <div class="name" v-text="item.videoTitle"></div>
               <div class="date">{{ item.createTime | dateFormat }}</div>
-              <div class="download" @click="downloadLink(item, $event)">
+              <div class="download" @click="download(item, $event)">
                 保存视频
               </div>
             </div>
@@ -66,7 +66,7 @@
 <script>
 import { mapGetters } from 'vuex'
 import { tabs } from '@/configs/tabs'
-import downloadLink from '@/utils/download-link'
+import downloadFile from '~/utils/donwload-tools'
 import { debounce } from '~/utils'
 
 export default {
@@ -102,9 +102,9 @@ export default {
       this.$refs.player.open()
     },
     // 下载方法
-    downloadLink(item, $event) {
+    download(item, $event) {
       const url = `${process.env.BASE_URL}/download/file?ossName=${item.videoDownloadUrl}&fileName=${item.videoName}`
-      downloadLink(url, $event)
+      downloadFile(url, item.videoName, this, $event)
     },
     // 获取列表
     fetchList: debounce(async function () {
@@ -224,9 +224,7 @@ export default {
         }
         .download {
           font-size: 16px;
-          @include themify($themes) {
-            color: themed('color');
-          }
+          color: #1890ff;
           cursor: pointer;
           margin-top: 8px;
         }

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

@@ -27,7 +27,7 @@
 <script>
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   data() {
     return {
       content: '',

+ 1 - 1
pages/_template/app/form/club-register.vue

@@ -87,7 +87,7 @@ import FormClubInfo from './components/form-club-info.vue'
 import FormClubDevice from './components/form-club-device.vue'
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   components: {
     SimpleStep,
     FormClubRegister,

+ 1 - 1
pages/_template/app/form/link-register.vue

@@ -36,7 +36,7 @@
 <script>
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   data() {
     return {
       authId: '',

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

@@ -10,7 +10,7 @@ import NormalHomePage from '@/views/NormalHomePage.vue'
 import RossHomePage from '@/views/RossHomePage.vue'
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   components: {
     NormalHomePage,
     RossHomePage,

+ 1 - 1
pages/_template/app/record/club/detail.vue

@@ -139,7 +139,7 @@
 import SimpleEmpty from '@/components/SimpleEmpty'
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   components: {
     SimpleEmpty,
   },

+ 1 - 1
pages/_template/app/record/club/edit.vue

@@ -25,7 +25,7 @@ import SimpleRadio from '@/components/SimpleRadio'
 import { mapGetters } from 'vuex'
 import FormClubInfo from '../../form/components/form-club-info.vue'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   components: {
     SimpleUploadImage,
     SimpleRadio,

+ 1 - 1
pages/_template/app/record/device/detail.vue

@@ -91,7 +91,7 @@
 <script>
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   data() {
     return {
       productId: '',

+ 1 - 1
pages/_template/app/record/device/edit.vue

@@ -27,7 +27,7 @@
 import FormClubDevice from '../../form/components/form-club-device.vue'
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   components: {
     FormClubDevice,
   },

+ 1 - 1
pages/_template/app/record/device/index.vue

@@ -51,7 +51,7 @@
 import SimpleEmpty from '@/components/SimpleEmpty'
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   components: {
     SimpleEmpty,
   },

+ 1 - 1
pages/_template/app/record/message.vue

@@ -14,7 +14,7 @@
 <script>
 import { mapGetters } from 'vuex'
 export default {
-  layout: 'app',
+  layout: 'app-ross',
   computed: {
     ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix', 'accessToken']),
   },

+ 4 - 4
pages/_template/ldm/database/package.vue

@@ -26,7 +26,7 @@
           >
             <div class="info">
               <div class="name" v-text="item.fileName"></div>
-              <div class="download" @click="downloadLink(item, $event)">
+              <div class="download" @click="download(item, $event)">
                 点击下载
               </div>
             </div>
@@ -42,7 +42,7 @@
 
 <script>
 import { mapGetters } from 'vuex'
-import downloadLink from '@/utils/download-link'
+import downloadFile from '~/utils/donwload-tools'
 import { debounce } from '~/utils'
 
 export default {
@@ -71,9 +71,9 @@ export default {
   },
   methods: {
     // 下载方法
-    downloadLink(item, $event) {
+    download(item, $event) {
       const url = `${process.env.BASE_URL}/download/file?ossName=${item.fileDownloadUrl}&fileName=${item.fileName}`
-      downloadLink(url, $event)
+      downloadFile(url, item.fileName, this, $event)
     },
     // 获取列表
     fetchList: debounce(async function () {

+ 509 - 0
pages/_template/ross/approve/club/detail.vue

@@ -0,0 +1,509 @@
+<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>
+      </div>
+    </div>
+    <div class="page-content">
+      <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">{{ 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="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="#f3920d"></SimpleMapNav>
+  </div>
+</template>
+
+<script>
+import { drawLogo } from '@/utils'
+import { mapNavigate } from '@/utils/map-utils'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-ross',
+  filters: {
+    formatEmpty(val) {
+      return val || '未知'
+    },
+    formatSnCode(code) {
+      if (!code) return ''
+      return code.replace(/^(\w{2})\w+(\w{4})$/, '$1******$2')
+    },
+  },
+  data() {
+    return {
+      authId: '',
+      clubInfo: {},
+    }
+  },
+  computed: {
+    ...mapGetters(['routePrefix']),
+    address() {
+      return (
+        (this.clubInfo.area &&
+          this.clubInfo.area + this.clubInfo.address &&
+          this.clubInfo.address) ||
+        '未知'
+      )
+    },
+    isEmpty() {
+      return this.clubInfo.productList
+        ? this.clubInfo.productList.length === 0
+        : true
+    },
+  },
+  mounted() {
+    this.initData()
+  },
+  beforeDestroy() {
+    this.$removeStorage(this.routePrefix, 'clubInfo')
+  },
+  methods: {
+    // 设备详情
+    toDetail(item) {
+      window.location.href = `${process.env.CIMEI_LOCAL}/product/auth/product-${item.productId}.html`
+    },
+    // 初始化
+    initData() {
+      this.authId = parseInt(this.$route.query.id)
+      const clubInfo = this.$getStorage(this.routePrefix, 'clubInfo')
+      if (clubInfo) {
+        this.clubInfo = clubInfo
+      }
+      this.fetchDetail()
+    },
+    // 获取机构详细信息
+    async fetchDetail() {
+      try {
+        const res = await this.$http.api.getAuthClubDetail({
+          authId: this.authId,
+        })
+        this.clubInfo = { ...this.clubInfo, ...res.data } // 合并
+      } catch (error) {
+        console.log(error)
+      }
+      // 默认轮播图
+      if (this.clubInfo.bannerList.length <= 0) {
+        this.clubInfo.bannerList.push('/placeholder.png')
+      }
+      // 默认logo
+      if (!this.clubInfo.logo) {
+        this.clubInfo.logo = drawLogo(this.clubInfo.authParty)
+      }
+    },
+    // 地图导航
+    onMapNav() {
+      this.$refs.mapNav.open()
+    },
+    // 导航
+    navigation(type) {
+      const point = this.clubInfo.lngAndLat.split(',')
+      const lng = point[0]
+      const lat = point[1]
+      mapNavigate(
+        { lat, lng, title: this.clubInfo.authParty, address: this.address },
+        type
+      )
+      this.$refs.mapNav.close()
+    },
+  },
+}
+</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;
+    overflow-y: auto;
+    .club-info {
+      padding: 32px 24px;
+      @include themify($themes) {
+        background: themed('cover-color');
+      }
+
+      .info {
+        width: 320px;
+        .name {
+          font-size: 24px;
+          color: #101010;
+          font-weight: bold;
+          margin-bottom: 34px;
+        }
+        .mobile,
+        .address {
+          position: relative;
+          padding-left: 24px;
+          margin-top: 16px;
+          line-height: 24px;
+          font-size: 16px;
+          color: #404040;
+
+          &::after {
+            content: '';
+            display: block;
+            width: 16px;
+            height: 16px;
+            position: absolute;
+            left: 0;
+            top: 4px;
+            background-size: 16px;
+            background-repeat: no-repeat;
+          }
+        }
+        .mobile {
+          &::after {
+            @include themify($themes) {
+              background-image: themed('pc-icon-mobile');
+            }
+          }
+        }
+        .address {
+          &::after {
+            @include themify($themes) {
+              background-image: themed('h5-icon-address');
+            }
+          }
+        }
+      }
+
+      .logo {
+        position: relative;
+        width: 114px;
+        height: 114px;
+        border-radius: 50% 50% 0 50%;
+        overflow: hidden;
+
+        &::after {
+          position: absolute;
+          bottom: 0;
+          right: 0;
+          content: '';
+          display: block;
+          width: 23px;
+          height: 23px;
+          background: url(~assets/theme-images/common/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;
+        @include themify($themes) {
+          color: themed('color');
+          border: 1px solid themed('color');
+          background-color: themed('sub-color');
+        }
+        cursor: pointer;
+
+        &::after {
+          content: '';
+          display: block;
+          width: 16px;
+          height: 16px;
+          margin-left: 4px;
+          @include themify($themes) {
+            background: themed('pc-icon-navigation') no-repeat center;
+          }
+          background-size: 16px;
+        }
+      }
+      .distance {
+        font-size: 14px;
+        color: #404040;
+      }
+    }
+
+    .device-list {
+      .title {
+        padding: 16px;
+        font-size: 20px;
+        font-weight: bold;
+        color: #404040;
+        background-color: #f3f5f6;
+      }
+
+      .list {
+        padding-right: 16px;
+      }
+
+      .device {
+        padding: 16px 0;
+        border-bottom: 1px solid #d8d8d8;
+        .info {
+          .name {
+            font-size: 18px;
+            color: #101010;
+          }
+          .code {
+            margin-top: 16px;
+            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;
+          @include themify($themes) {
+            background: themed('color');
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@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;
+    }
+    .club-info {
+      padding: 4vw;
+      @include themify($themes) {
+        background: themed('cover-color');
+      }
+
+      .info {
+        width: 67vw;
+        .name {
+          font-size: 4.8vw;
+          color: #101010;
+          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: #404040;
+
+          &::after {
+            content: '';
+            display: block;
+            width: 4vw;
+            height: 4vw;
+            position: absolute;
+            left: 0;
+            top: 0.5vw;
+            background-size: 4vw 4vw;
+            background-repeat: no-repeat;
+          }
+        }
+        .mobile {
+          &::after {
+            @include themify($themes) {
+              background-image: themed('h5-icon-mobile');
+            }
+          }
+        }
+        .address {
+          &::after {
+            @include themify($themes) {
+              background-image: themed('h5-icon-address');
+            }
+          }
+        }
+      }
+
+      .logo {
+        position: relative;
+        width: 18vw;
+        height: 18vw;
+        border-radius: 9vw 9vw 0 9vw;
+        overflow: hidden;
+
+        &::after {
+          position: absolute;
+          bottom: 0;
+          right: 0;
+          content: '';
+          display: block;
+          width: 3.6vw;
+          height: 3.6vw;
+          background: url(~assets/theme-images/common/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;
+        @include themify($themes) {
+          color: themed('color');
+          border: 1px solid themed('color');
+          background-color: themed('sub-color');
+        }
+
+        &::after {
+          content: '';
+          display: block;
+          width: 3.6vw;
+          height: 3.6vw;
+          margin-left: 0.4vw;
+          @include themify($themes) {
+            background: themed('h5-icon-navigation') no-repeat center;
+            background-size: 3.6vw;
+          }
+        }
+      }
+      .distance {
+        font-size: 3vw;
+        color: #404040;
+      }
+    }
+
+    .device-list {
+      .title {
+        padding: 4vw;
+        padding-bottom: 0;
+        font-size: 4vw;
+        font-weight: bold;
+        color: #101010;
+      }
+      .device {
+        padding: 4vw 0;
+        margin: 0 4vw;
+        border-bottom: 0.4vw solid #d8d8d8;
+        .info {
+          .name {
+            font-size: 3.6vw;
+            color: #101010;
+          }
+          .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;
+          @include themify($themes) {
+            background: themed('color');
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 273 - 0
pages/_template/ross/approve/device/index.vue

@@ -0,0 +1,273 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <div class="mt-2 name">
+          <span v-text="supplierInfo.shopName"></span><span>官方授权设备</span>
+        </div>
+      </div>
+      <div class="page-content">
+        <!-- 标题 -->
+        <div class="title px-4 pt-12 pb-6">
+          共<span v-text="total"></span>种设备
+        </div>
+        <!-- 列表 -->
+        <div class="list">
+          <div
+            class="section flex items-center mb-4"
+            v-for="item in list"
+            :key="item.productTypeId"
+            @click="toDetail(item)"
+          >
+            <img class="cover" :src="item.image" />
+            <div class="name" v-text="item.name"></div>
+          </div>
+        </div>
+
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          name="icon-empty-device.png"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { debounce } from '@/utils'
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      listQuery: {
+        authUserId: '',
+        name: '',
+        pageNum: 1,
+        pageSize: 9,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 获取设备分类
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        this.listQuery.authUserId = this.authUserId
+        const res = await this.$http.api.getAuthProductCateList(this.listQuery)
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.total = res.data.total
+        this.isLoadingMore = false
+        this.listQuery.pageNum += 1
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    }, 400),
+
+    // 设备详情
+    toDetail(item) {
+      const url = `${this.routePrefix}/approve/device/search?id=${item.productTypeId}`
+      this.$router.push(url)
+    },
+    // 搜索
+    onSearch() {
+      this.listQuery.pageNum = 1
+      this.list = []
+      this.fetchList()
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+  }
+  .page-top {
+    height: 420px;
+    @include themify($themes) {
+      background: themed('pc-banner-device');
+      background-size: auto 420px;
+    }
+
+    .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 {
+      position: absolute;
+      left: 50%;
+      top: 300px;
+      transform: translateX(-50%);
+    }
+
+    .title {
+      font-size: 16px;
+      color: #404040;
+
+      span {
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .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;
+        }
+        .name {
+          width: 260px;
+          font-size: 18px;
+          color: #101010;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          overflow: hidden;
+          margin-left: 16px;
+          font-weight: bold;
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('h5-banner-device');
+      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%);
+    }
+
+    .title {
+      font-size: 3.4vw;
+      color: #404040;
+
+      span {
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .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;
+        }
+        .name {
+          width: 66vw;
+          font-size: 4vw;
+          color: #101010;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          overflow: hidden;
+          margin-left: 3.2vw;
+        }
+      }
+    }
+  }
+}
+</style>

+ 324 - 0
pages/_template/ross/approve/device/list.vue

@@ -0,0 +1,324 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <div class="mt-2 name">
+          <span v-text="supplierInfo.shopName"></span><span>官方授权设备</span>
+        </div>
+      </div>
+      <div class="page-content">
+        <!-- 标题 -->
+        <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>
+              <div class="club-name">
+                所属机构:<span @click.stop="toClubDetail(item)">{{
+                  item.clubName
+                }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          name="icon-empty-device.png"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { debounce } from '@/utils'
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-ross',
+  filters: {
+    formatSnCode(code) {
+      if (!code) return ''
+      return code.replace(/^(\w{2})\w+(\w{4})$/, '$1******$2')
+    },
+  },
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      listQuery: {
+        productTypeId: '',
+        snCode: '',
+        authParty: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['routePrefix', 'supplierInfo']),
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      const { id, keyword, type } = this.$route.query
+      this.listQuery.productTypeId = parseInt(id)
+      if (type === '1') {
+        this.listQuery.snCode = decodeURIComponent(keyword)
+      } else {
+        this.listQuery.authParty = decodeURIComponent(keyword)
+      }
+      this.fetchList()
+    },
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        const res = await this.$http.api.getAuthProductList(this.listQuery)
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.total = res.data.total
+        this.isLoadingMore = false
+        this.listQuery.pageNum += 1
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    }, 400),
+    // 搜索
+    onSearch() {
+      this.list = []
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 设备详情
+    toDetail(item) {
+      window.location.href = `${process.env.CIMEI_LOCAL}/product/auth/product-${item.productId}.html`
+    },
+    // 机构详情
+    toClubDetail(item) {
+      const url = `${this.routePrefix}/approve/club/detail?id=${item.authId}`
+      this.$router.push(url)
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+  }
+  .page-top {
+    height: 420px;
+    @include themify($themes) {
+      background: themed('pc-banner-device');
+    }
+    background-size: auto 420px;
+
+    .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;
+
+    .title {
+      font-size: 16px;
+      color: #404040;
+
+      span {
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .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;
+    @include themify($themes) {
+      background: themed('h5-banner-device');
+      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;
+
+    .title {
+      font-size: 3.4vw;
+      color: #404040;
+
+      span {
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .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 {
+            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>

+ 227 - 0
pages/_template/ross/approve/device/search.vue

@@ -0,0 +1,227 @@
+<template>
+  <div class="page">
+    <div class="page-top flex flex-col justify-center items-center">
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="mt-2 name">
+        <span v-text="supplierInfo.shopName"></span><span>官方授权设备</span>
+      </div>
+    </div>
+    <div class="page-content">
+      <div class="title">SN码或机构名称:</div>
+      <el-input
+        placeholder="请输入设备SN码或机构名称搜索查询"
+        v-model="keyword"
+        class="input-with-select"
+      >
+        <el-select
+          v-model="type"
+          slot="prepend"
+          placeholder="请选择"
+          class="select-with-input"
+        >
+          <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>
+      <div class="button flex justify-center items-center" @click="onSearch">
+        查询
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      keyword: '',
+      type: '1',
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix']),
+  },
+  methods: {
+    onSearch() {
+      if (!this.keyword) {
+        this.$toast('请输入要查询的内容')
+        return
+      }
+      this.$router.push(
+        `${this.routePrefix}/approve/device/list?type=${
+          this.type
+        }&keyword=${encodeURIComponent(this.keyword)}&id=${
+          this.$route.query.id
+        }`
+      )
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.el-input {
+  & > ::v-deep {
+    .el-input.is-active .el-input__inner,
+    .el-input__inner:focus {
+      @include themify($themes) {
+        border-color: themed('color');
+      }
+    }
+  }
+}
+
+.input-with-select {
+  ::v-deep {
+    .el-input-group__prepend {
+      background-color: #fff;
+    }
+  }
+}
+
+// pc 端
+@media screen and (min-width: 768px) {
+  .el-input {
+    ::v-deep {
+      .el-select .el-input {
+        width: 130px;
+      }
+    }
+  }
+
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+  }
+  .page-top {
+    height: 420px;
+    @include themify($themes) {
+      background: themed('pc-banner-device');
+    }
+    background-size: auto 420px;
+
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 518px;
+    margin: 0 auto;
+
+    .title {
+      font-size: 18px;
+      line-height: 24px;
+      color: #282828;
+      margin: 68px 0 12px;
+    }
+
+    .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;
+          font-size: 18px;
+        }
+      }
+    }
+
+    .button {
+      width: 295px;
+      height: 50px;
+      margin: 0 auto;
+      border-radius: 4px;
+      margin-top: 90px;
+      cursor: pointer;
+      @include themify($themes) {
+        background-color: themed('color');
+        color: #fff;
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .el-input {
+    ::v-deep {
+      .select-with-input .el-input {
+        width: 22vw;
+
+        .el-input__inner {
+          padding-left: 2.4vw;
+        }
+      }
+    }
+  }
+
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('h5-banner-device');
+      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;
+    padding: 0 4.2vw;
+
+    .title {
+      font-size: 3.4vw;
+      line-height: 4.5vw;
+      color: #666;
+      margin: 18vw 0 1.6vw;
+    }
+
+    .el-input {
+      font-size: 3.4vw;
+      .el-input__icon {
+        font-size: 5.4vw;
+      }
+    }
+
+    .button {
+      height: 12vw;
+      margin: 0 auto;
+      border-radius: 2px;
+      margin-top: 12.8vw;
+      font-size: 3.6vw;
+      cursor: pointer;
+      @include themify($themes) {
+        background-color: themed('color');
+        color: #fff;
+      }
+    }
+  }
+}
+</style>

+ 333 - 0
pages/_template/ross/approve/personnel/operate/detail.vue

@@ -0,0 +1,333 @@
+<template>
+  <div class="page md:flex md:justify-between">
+    <div class="page-title">{{ themeName === 'ross' ? '体疗师认证' : '医师认证' }}</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 { mapGetters } from 'vuex'
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      doctorId: '',
+      doctorInfo: {
+        tagList: [],
+        paramList: [],
+      },
+    }
+  },
+  computed:{
+    ...mapGetters(['themeName'])
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      this.doctorId = parseInt(this.$route.query.id)
+      this.fetchDetail()
+    },
+    async fetchDetail() {
+      try {
+        const res = await this.$http.api.fetchDoctorDetail({
+          doctorId: this.doctorId,
+        })
+        this.doctorInfo = { ...this.clubInfo, ...res.data }
+      } catch (error) {
+        console.log(error)
+      }
+      if (this.doctorInfo.bannerList.length <= 0) {
+        this.doctorInfo.bannerList.push('/placeholder.png')
+      }
+    },
+  },
+}
+</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>

+ 350 - 0
pages/_template/ross/approve/personnel/operate/index.vue

@@ -0,0 +1,350 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <div class="mt-2 name">
+          <span v-text="supplierInfo.shopName"></span><span v-if="themeName === 'ross'">官方体疗师认证</span><span v-else>官方医师认证</span>
+        </div>
+      </div>
+      <div class="page-content">
+        <!-- 搜索区域 -->
+        <div class="search">
+          <SimpleSearch
+            v-model="listQuery.doctorName"
+            @search="onSearch"
+            :placeholder="searchPlaceholder"
+          />
+        </div>
+        <!-- 标题 -->
+        <div class="title px-4 pt-12 pb-6">
+          共<span v-text="total"></span>位{{
+            themeName === 'ross' ? '体疗师' : '医师'
+          }}
+        </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 { mapGetters } from 'vuex'
+import { debounce } from '@/utils'
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      listQuery: {
+        authUserId: '',
+        doctorType: 1,
+        doctorName: '',
+        pageNum: 1,
+        pageSize: 9,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix', 'themeName']),
+    searchPlaceholder() {
+      return this.themeName === 'ross' ? '搜索体疗师' : '搜索医师'
+    },
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        this.listQuery.authUserId = this.authUserId
+        const res = await this.$http.api.fetchDoctorList(this.listQuery)
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.total = res.data.total
+        this.listQuery.pageNum += 1
+        this.isLoadingMore = false
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    }, 400),
+
+    // 搜索
+    onSearch() {
+      this.list = []
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 医师详情
+    toDetail(item) {
+      const url = `${this.routePrefix}/approve/personnel/operate/detail?id=${item.doctorId}`
+      this.$router.push(url)
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</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;
+    @include themify($themes) {
+      background: themed('pc-banner-doctor') 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 {
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .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;
+    @include themify($themes) {
+      background: themed('h5-banner-doctor') 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 {
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .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>

+ 116 - 0
pages/_template/ross/database/article-detail.vue

@@ -0,0 +1,116 @@
+<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>
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      articleId: '',
+      articleInfo: {},
+      imageList: [],
+    }
+  },
+  computed: {
+    html() {
+      const html = this.articleInfo.articleContent
+      if (html) {
+        return html.replace(/href=/gi, '')
+      }
+      return ''
+    },
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      this.articleId = parseInt(this.$route.query.id)
+      this.fetchArticleDetail()
+    },
+    async fetchArticleDetail() {
+      try {
+        const res = await this.$http.api.getArticleDetail({
+          articleId: this.articleId,
+        })
+        this.articleInfo = res.data
+      } catch (error) {
+        console.log(error)
+      }
+    },
+  },
+}
+</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>

+ 303 - 0
pages/_template/ross/database/article.vue

@@ -0,0 +1,303 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <span
+          class="name mt-2"
+          v-text="supplierInfo.shopName + '资料库'"
+        ></span>
+      </div>
+      <div class="page-content">
+        <!-- 搜索区域 -->
+        <div class="search">
+          <simple-search
+            v-model="listQuery.articleTitle"
+            @search="onSearch"
+            placeholder="搜索文章标题"
+          />
+        </div>
+        <div class="divider"></div>
+        <!-- tabbar -->
+        <simple-tabs
+          :tabs="tabs"
+          :current="current"
+          @change="onTabChange"
+          @search="onSearch"
+        ></simple-tabs>
+        <div class="list">
+          <div
+            class="section flex justify-between items-center"
+            v-for="item in list"
+            :key="item.articleId"
+            @click="toDetail(item)"
+          >
+            <div class="info">
+              <div class="name" v-text="item.articleTitle"></div>
+              <div class="date">{{ item.createTime | dateFormat }}</div>
+            </div>
+            <img class="cover" :src="item.articleImage" />
+          </div>
+        </div>
+
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+import { debounce } from '~/utils'
+
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      tabs: tabs(),
+      current: 0,
+      listQuery: {
+        articleTitle: '',
+        authUserId: '102',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'authUserId', 'routePrefix']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        this.listQuery.authUserId = this.authUserId
+        const res = await this.$http.api.getArticleList(this.listQuery)
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.total = res.data.total
+        this.isLoadingMore = false
+        this.listQuery.pageNum += 1
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    }, 400),
+
+    // 详情
+    toDetail(item) {
+      const url = `${this.routePrefix}/database/article-detail?id=${item.articleId}`
+      this.$router.push(url)
+    },
+    // tab切换
+    onTabChange(item) {
+      this.$router.push(`${this.routePrefix}${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.list = []
+      this.listQuery.articleTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</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-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('pc-banner-doc');
+    }
+    background-size: auto 360px;
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1200px;
+    margin: 0 auto;
+    background-color: #fff;
+    .divider {
+      height: 16px;
+      background: #f7f7f7;
+    }
+    .search {
+      display: none;
+    }
+  }
+  .list {
+    border-top: 16px solid #f7f7f7;
+    border-bottom: 16px solid #f7f7f7;
+    .section {
+      padding: 32px 0;
+      margin: 0 24px;
+      background: #fff;
+      border-bottom: 1px solid #d8d8d8;
+      transition: all 0.4s;
+      cursor: pointer;
+
+      &:hover {
+        .name {
+          @include themify($themes) {
+            color: themed('color') !important;
+          }
+        }
+      }
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      .info {
+        width: 1000px;
+        height: 100%;
+
+        .name {
+          font-size: 18px;
+          color: #404040;
+          line-height: 1.6;
+          margin-bottom: 18px;
+          text-align: justify;
+          @include ellipsis(2);
+        }
+        .date {
+          font-size: 18px;
+          color: #b2b2b2;
+          line-height: 1.4;
+        }
+      }
+
+      .cover {
+        width: 106px;
+        height: 106px;
+        border-bottom: 1px solid #d8d8d8;
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('h5-banner-doc');
+    }
+    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;
+    .divider {
+      border-bottom: 3.2vw solid #f7f7f7;
+      height: 12.4vw;
+    }
+  }
+
+  .search {
+    position: absolute;
+    left: 50%;
+    top: 0;
+    transform: translate(-50%, -50%);
+  }
+
+  .list {
+    .section {
+      padding: 2.4vw 0;
+      margin: 0 4vw;
+      background: #fff;
+      border-bottom: 0.1vw solid #d8d8d8;
+
+      &:first-child {
+        border-top: 0.1vw solid #d8d8d8;
+      }
+
+      .info {
+        width: 67.3vw;
+        height: 100%;
+
+        .name {
+          font-size: 3.6vw;
+          color: #404040;
+          line-height: 1.5;
+          text-align: justify;
+          margin-bottom: 2.4vw;
+          @include ellipsis(2);
+        }
+        .date {
+          font-size: 3.2vw;
+          color: #b2b2b2;
+          line-height: 1.4;
+        }
+      }
+
+      .cover {
+        width: 18vw;
+        height: 18vw;
+        border-bottom: 0.1vw solid #d8d8d8;
+      }
+    }
+  }
+}
+</style>

+ 334 - 0
pages/_template/ross/database/file.vue

@@ -0,0 +1,334 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <span
+          class="name mt-2"
+          v-text="supplierInfo.shopName + '资料库'"
+        ></span>
+      </div>
+      <div class="page-content">
+        <!-- 搜索区域 -->
+        <div class="search">
+          <simple-search
+            v-model="listQuery.fileTitle"
+            @search="onSearch"
+            placeholder="搜索文件名称"
+          />
+        </div>
+        <div class="divider"></div>
+        <!-- tabbar -->
+        <simple-tabs
+          :tabs="tabs"
+          :current="current"
+          @change="onTabChange"
+          @search="onSearch"
+        ></simple-tabs>
+        <div class="list">
+          <div
+            class="section md:flex md:justify-between md:items-center"
+            v-for="item in list"
+            :key="item.fileId"
+            @click="previewFile(item)"
+          >
+            <div class="info">
+              <div class="name" v-text="item.fileName"></div>
+              <div class="date">{{ item.createTime | dateFormat }}</div>
+              <div class="download" @click.stop="download(item, $event)">
+                下载
+              </div>
+            </div>
+          </div>
+        </div>
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { tabs } from '~/configs/tabs'
+import downloadFile from '~/utils/donwload-tools'
+import { debounce } from '~/utils'
+
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      tabs: tabs(),
+      current: 3,
+      listQuery: {
+        fileType: 1,
+        fileTitle: '',
+        authUserId: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['routePrefix', 'supplierInfo', 'authUserId']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 下载方法
+    download(item, $event) {
+      const url = `${process.env.BASE_URL}/download/file?ossName=${item.fileDownloadUrl}&fileName=${item.fileName}`
+      downloadFile(url, item.fileName, this, $event)
+    },
+    // 预览文件
+    previewFile(item) {
+      window.open(item.filePreviewUrl)
+    },
+    // 获取列表
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        this.listQuery.authUserId = this.authUserId
+        const res = await this.$http.api.getFileList(this.listQuery)
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.total = res.data.total
+        this.isLoadingMore = false
+        this.listQuery.pageNum += 1
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    }, 400),
+    // tab切换
+    onTabChange(item) {
+      this.$router.push(`${this.routePrefix}${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.list = []
+      this.listQuery.fileTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</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-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('pc-banner-doc');
+    }
+    background-size: auto 360px;
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1200px;
+    margin: 0 auto;
+    background-color: #fff;
+    .divider {
+      height: 16px;
+      background: #f7f7f7;
+    }
+    .search {
+      display: none;
+    }
+  }
+  .list {
+    border-top: 16px solid #f7f7f7;
+    border-bottom: 16px solid #f7f7f7;
+    .section {
+      padding: 32px 0;
+      margin: 0 24px;
+      background: #fff;
+      border-bottom: 1px solid #d8d8d8;
+      transition: all 0.4s;
+      cursor: pointer;
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      &:hover {
+        .name {
+          @include themify($themes) {
+            color: themed('color') !important;
+          }
+        }
+      }
+
+      .info {
+        width: 100%;
+        position: relative;
+        .name {
+          width: 1050px;
+          height: 56px;
+          font-size: 18px;
+          color: #404040;
+          line-height: 1.6;
+          margin-bottom: 18px;
+          text-align: justify;
+          @include ellipsis(2);
+          transition: all 0.4s;
+        }
+        .date {
+          font-size: 18px;
+          color: #b2b2b2;
+          line-height: 1.4;
+        }
+        .download {
+          position: absolute;
+          text-align: center;
+          right: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          font-size: 16px;
+          color: #1890ff;
+          cursor: pointer;
+
+          &::before {
+            content: '';
+            display: block;
+            width: 54px;
+            height: 59px;
+            background: url(~assets/theme-images/common/pc-icon-pdf-download.png)
+              no-repeat center;
+            background-size: 54px;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('h5-banner-doc');
+    }
+    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;
+    .divider {
+      border-bottom: 3.2vw solid #f7f7f7;
+      height: 12.4vw;
+    }
+  }
+
+  .search {
+    position: absolute;
+    left: 50%;
+    top: 0;
+    transform: translate(-50%, -50%);
+  }
+
+  .list {
+    .section {
+      position: relative;
+      padding: 2.4vw 0;
+      margin: 0 4vw;
+      padding-bottom: 6.8vw;
+      background: #fff;
+      border-bottom: 0.1vw solid #d8d8d8;
+
+      &:first-child {
+        border-top: 0.1vw solid #d8d8d8;
+      }
+
+      .info {
+        .name {
+          font-size: 3.6vw;
+          color: #404040;
+          line-height: 1.5;
+          margin-bottom: 2.4vw;
+          text-align: justify;
+          @include ellipsis(2);
+        }
+        .date {
+          position: absolute;
+          left: 0;
+          bottom: 2.4vw;
+          font-size: 3.2vw;
+          color: #b2b2b2;
+          line-height: 1.4;
+        }
+
+        .download {
+          position: absolute;
+          right: 0;
+          bottom: 2.4vw;
+          font-size: 3.2vw;
+          @include themify($themes) {
+            color: themed('color');
+          }
+          cursor: pointer;
+          margin-top: 1.2vw;
+        }
+      }
+    }
+  }
+}
+</style>

+ 348 - 0
pages/_template/ross/database/image.vue

@@ -0,0 +1,348 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <span
+          class="name mt-2"
+          v-text="supplierInfo.shopName + '资料库'"
+        ></span>
+      </div>
+      <div class="page-content">
+        <!-- 搜索区域 -->
+        <div class="search">
+          <simple-search
+            v-model="listQuery.imageTitle"
+            @search="onSearch"
+            placeholder="搜索图片标题"
+          />
+        </div>
+        <div class="divider"></div>
+        <!-- tabbar -->
+        <simple-tabs
+          :tabs="tabs"
+          :current="current"
+          @change="onTabChange"
+          @search="onSearch"
+        ></simple-tabs>
+        <div class="list">
+          <div class="section" v-for="item in list" :key="item.imageId">
+            <div class="info">
+              <div class="name" v-text="item.imageTitle"></div>
+              <div class="date">{{ item.createTime | dateFormat }}</div>
+              <div class="download" @click="download(item.imageZipUrl, $event)">
+                保存所有图片
+              </div>
+            </div>
+            <div class="images grid grid-cols-4 md:grid-cols-8 gap-3 md:gap-4">
+              <div
+                class="item"
+                v-for="(image, index) in item.imageList"
+                :key="index"
+                @click="onImagePreview(item.imageList, index)"
+              >
+                <img class="object-cover" :src="image" />
+              </div>
+            </div>
+          </div>
+        </div>
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+import downloadFile from '~/utils/donwload-tools'
+import { ImagePreview } from 'vant'
+import { debounce } from '~/utils'
+
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      tabs: tabs(),
+      current: 1,
+      listQuery: {
+        imageTitle: '',
+        authUserId: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['routePrefix', 'supplierInfo', 'authUserId']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 下载方法
+    download(url, $event) {
+      downloadFile(url, '图片包', this, $event)
+    },
+    // 获取列表
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        this.listQuery.authUserId = this.authUserId
+        const res = await this.$http.api.getImageList(this.listQuery)
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.total = res.data.total
+        this.isLoadingMore = false
+        this.listQuery.pageNum += 1
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    }, 400),
+
+    // 图片预览
+    onImagePreview(imageList, index) {
+      ImagePreview({
+        images: imageList,
+        startPosition: index,
+        loop: true,
+        showIndex: true,
+        showIndicators: true,
+        closeable: true,
+      })
+    },
+    // tab切换
+    onTabChange(item) {
+      this.$router.push(`${this.routePrefix}${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.list = []
+      this.listQuery.imageTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</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-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('pc-banner-doc');
+    }
+    background-size: auto 360px;
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1200px;
+    margin: 0 auto;
+    background-color: #fff;
+    .divider {
+      height: 16px;
+      background: #f7f7f7;
+    }
+    .search {
+      display: none;
+    }
+  }
+  .list {
+    border-top: 16px solid #f7f7f7;
+    border-bottom: 16px solid #f7f7f7;
+    .section {
+      padding: 32px 0;
+      margin: 0 24px;
+      background: #fff;
+      border-bottom: 1px solid #d8d8d8;
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      .info {
+        position: relative;
+        .name {
+          font-size: 18px;
+          color: #404040;
+          line-height: 1.6;
+          margin-bottom: 18px;
+          text-align: justify;
+        }
+        .date {
+          font-size: 18px;
+          color: #b2b2b2;
+          line-height: 1.4;
+        }
+
+        .download {
+          bottom: 0;
+          right: 0;
+          position: absolute;
+          color: #1890ff;
+          font-size: 16px;
+          cursor: pointer;
+        }
+      }
+
+      .images {
+        margin-top: 24px;
+        .item {
+          width: 126px;
+          height: 126px;
+          overflow: hidden;
+          img {
+            display: block;
+            width: 100%;
+            height: 100%;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('h5-banner-doc');
+    }
+    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;
+    .divider {
+      border-bottom: 3.2vw solid #f7f7f7;
+      height: 12.4vw;
+    }
+  }
+
+  .search {
+    position: absolute;
+    left: 50%;
+    top: 0;
+    transform: translate(-50%, -50%);
+  }
+
+  .list {
+    .section {
+      position: relative;
+      padding: 2.4vw 0;
+      margin: 0 4vw;
+      padding-bottom: 9.8vw;
+      background: #fff;
+      border-bottom: 0.1vw solid #d8d8d8;
+
+      &:first-child {
+        border-top: 0.1vw solid #d8d8d8;
+      }
+
+      .info {
+        .name {
+          font-size: 3.6vw;
+          color: #404040;
+          line-height: 1.5;
+          margin-bottom: 2.4vw;
+          @include ellipsis(2);
+          text-align: justify;
+        }
+        .date {
+          bottom: 2.4vw;
+          left: 0;
+          position: absolute;
+          font-size: 3.2vw;
+          color: #b2b2b2;
+          line-height: 1.4;
+        }
+
+        .download {
+          bottom: 2.4vw;
+          right: 0;
+          position: absolute;
+          @include themify($themes) {
+            color: themed('color');
+          }
+          font-size: 3.6vw;
+          cursor: pointer;
+        }
+      }
+
+      .images {
+        margin-top: 2.4vw;
+        .item {
+          width: 21vw;
+          height: 21vw;
+          overflow: hidden;
+          img {
+            display: block;
+            width: 100%;
+            height: 100%;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 302 - 0
pages/_template/ross/database/package.vue

@@ -0,0 +1,302 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <span
+          class="name mt-2"
+          v-text="supplierInfo.shopName + '资料库'"
+        ></span>
+      </div>
+      <div class="page-content">
+        <!-- 搜索区域 -->
+        <div class="search">
+          <simple-search
+            v-model="listQuery.fileTitle"
+            @search="onSearch"
+            placeholder="搜索资料包"
+          />
+        </div>
+        <div class="divider"></div>
+        <!-- tabbar -->
+        <simple-tabs
+          :tabs="tabs"
+          :current="current"
+          @change="onTabChange"
+          @search="onSearch"
+        ></simple-tabs>
+        <div class="list">
+          <div
+            class="section flex justify-between items-center"
+            v-for="item in list"
+            :key="item.fileId"
+          >
+            <div class="info">
+              <div class="name" v-text="item.fileName"></div>
+              <div class="download" @click="download(item, $event)">
+                点击下载
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+import downloadFile from '~/utils/donwload-tools'
+import { debounce } from '~/utils'
+
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      tabs: tabs(),
+      current: 4,
+      listQuery: {
+        fileType: 2,
+        fileTitle: '',
+        authUserId: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['routePrefix', 'supplierInfo', 'authUserId']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 下载方法
+    download(item, $event) {
+      const url = `${process.env.BASE_URL}/download/file?ossName=${item.fileDownloadUrl}&fileName=${item.fileName}`
+      downloadFile(url, item.fileName, this, $event)
+    },
+    // 获取列表
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        this.listQuery.authUserId = this.authUserId
+        const res = await this.$http.api.getFileList(this.listQuery)
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.total = res.data.total
+        this.isLoadingMore = false
+        this.listQuery.pageNum += 1
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    }, 400),
+
+    // tab切换
+    onTabChange(item) {
+      this.$router.push(`${this.routePrefix}${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.list = []
+      this.listQuery.fileTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</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-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('pc-banner-doc');
+    }
+    background-size: auto 360px;
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1200px;
+    margin: 0 auto;
+    background-color: #fff;
+    .divider {
+      height: 16px;
+      background: #f7f7f7;
+    }
+    .search {
+      display: none;
+    }
+  }
+  .list {
+    border-top: 16px solid #f7f7f7;
+    border-bottom: 16px solid #f7f7f7;
+    .section {
+      padding: 32px 0;
+      margin: 0 24px;
+      background: #fff;
+      border-bottom: 1px solid #d8d8d8;
+      transition: all 0.4s;
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      .info {
+        width: 100%;
+        position: relative;
+        .name {
+          width: 1050px;
+          font-size: 18px;
+          color: #404040;
+          line-height: 1.6;
+          text-align: justify;
+          @include ellipsis(2);
+        }
+        .download {
+          position: absolute;
+          right: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          font-size: 16px;
+          color: #1890ff;
+          cursor: pointer;
+          &::after {
+            content: '>';
+            margin-left: 8px;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('h5-banner-doc');
+    }
+    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;
+    .divider {
+      border-bottom: 3.2vw solid #f7f7f7;
+      height: 12.4vw;
+    }
+  }
+
+  .search {
+    position: absolute;
+    left: 50%;
+    top: 0;
+    transform: translate(-50%, -50%);
+  }
+
+  .list {
+    .section {
+      position: relative;
+      padding: 4.8vw 0;
+      margin: 0 4vw;
+      background: #fff;
+      border-bottom: 0.1vw solid #d8d8d8;
+
+      &:first-child {
+        border-top: 0.1vw solid #d8d8d8;
+      }
+
+      .info {
+        .name {
+          width: 66vw;
+          font-size: 3.6vw;
+          color: #404040;
+          line-height: 1.5;
+          text-align: justify;
+          @include ellipsis(2);
+        }
+
+        .download {
+          position: absolute;
+          right: 0;
+          bottom: 50%;
+          transform: translateY(50%);
+          font-size: 3.2vw;
+          @include themify($themes) {
+            color: themed('color');
+          }
+          cursor: pointer;
+
+          &::after {
+            content: '>';
+            margin-left: 8px;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 361 - 0
pages/_template/ross/database/video.vue

@@ -0,0 +1,361 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center">
+        <img class="logo" :src="supplierInfo.logo" />
+        <span
+          class="name mt-2"
+          v-text="supplierInfo.shopName + '资料库'"
+        ></span>
+      </div>
+      <div class="page-content">
+        <!-- 搜索区域 -->
+        <div class="search">
+          <simple-search
+            v-model="listQuery.videoTitle"
+            @search="onSearch"
+            placeholder="搜索视频"
+          />
+        </div>
+        <div class="divider"></div>
+        <!-- tabbar -->
+        <simple-tabs
+          :tabs="tabs"
+          :current="current"
+          @change="onTabChange"
+          @search="onSearch"
+        ></simple-tabs>
+        <div class="list">
+          <div
+            class="section md:flex md:justify-between md:items-center"
+            v-for="item in list"
+            :key="item.videoId"
+          >
+            <div class="info">
+              <div class="name" v-text="item.videoTitle"></div>
+              <div class="date">{{ item.createTime | dateFormat }}</div>
+              <div class="download" @click="download(item, $event)">
+                保存视频
+              </div>
+            </div>
+            <div class="cover">
+              <video class="cover" :src="item.videoPreviewUrl"></video>
+              <span @click="onPlayer(item)"></span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 列表为空 -->
+        <SimpleEmpty
+          v-if="!total && !isRequest"
+          description="敬请期待~"
+        ></SimpleEmpty>
+      </div>
+    </van-list>
+
+    <SimpleVideoPlayer ref="player" :videoSrc="videoUrl"></SimpleVideoPlayer>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+import downloadFile from '~/utils/donwload-tools'
+import { debounce } from '~/utils'
+
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      tabs: tabs(),
+      current: 2,
+      listQuery: {
+        videoTitle: '',
+        authUserId: '102',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+      videoUrl: '',
+    }
+  },
+  computed: {
+    ...mapGetters(['routePrefix', 'supplierInfo', 'authUserId']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 视频在线播放
+    onPlayer(item) {
+      this.videoUrl = item.videoPreviewUrl
+      this.$refs.player.open()
+    },
+    // 下载方法
+    download(item, $event) {
+      const url = `${process.env.BASE_URL}/download/file?ossName=${item.videoDownloadUrl}&fileName=${item.videoName}`
+      downloadFile(url, item.videoName, this, $event)
+    },
+    // 获取列表
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        this.listQuery.authUserId = this.authUserId
+        const res = await this.$http.api.getVideoList(this.listQuery)
+        this.list = [...this.list, ...res.data.list]
+        this.finished = !res.data.hasNextPage
+        this.listQuery.pageNum += 1
+        this.isLoadingMore = false
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    }, 400),
+
+    // tab切换
+    onTabChange(item) {
+      this.$router.push(`${this.routePrefix}${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.list = []
+      this.listQuery.videoTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</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-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('pc-banner-doc');
+    }
+    background-size: auto 360px;
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1200px;
+    margin: 0 auto;
+    background-color: #fff;
+    .divider {
+      height: 16px;
+      background: #f7f7f7;
+    }
+    .search {
+      display: none;
+    }
+  }
+  .list {
+    border-top: 16px solid #f7f7f7;
+    border-bottom: 16px solid #f7f7f7;
+    .section {
+      padding: 32px 0;
+      margin: 0 24px;
+      background: #fff;
+      border-bottom: 1px solid #d8d8d8;
+      transition: all 0.4s;
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      .info {
+        width: 880px;
+        height: 100%;
+
+        .name {
+          height: 56px;
+          font-size: 18px;
+          color: #404040;
+          line-height: 1.6;
+          margin-bottom: 18px;
+          @include ellipsis(2);
+          text-align: justify;
+        }
+        .date {
+          font-size: 18px;
+          color: #b2b2b2;
+          line-height: 1.4;
+        }
+        .download {
+          font-size: 16px;
+          color: #1890ff;
+          cursor: pointer;
+          margin-top: 8px;
+        }
+      }
+
+      .cover {
+        position: relative;
+        width: 216px;
+        height: 132px;
+        background: #333;
+        span {
+          position: absolute;
+          width: 36px;
+          height: 36px;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          background: url(~assets/theme-images/common/h5-icon-play.png)
+            no-repeat center;
+          background-size: 36px;
+          border-radius: 50%;
+          cursor: pointer;
+          box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('h5-banner-doc');
+    }
+    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;
+    .divider {
+      border-bottom: 3.2vw solid #f7f7f7;
+      height: 12.4vw;
+    }
+  }
+
+  .search {
+    position: absolute;
+    left: 50%;
+    top: 0;
+    transform: translate(-50%, -50%);
+  }
+
+  .list {
+    .section {
+      position: relative;
+      padding: 2.4vw 0;
+      margin: 0 4vw;
+      padding-bottom: 9.8vw;
+      background: #fff;
+      border-bottom: 0.1vw solid #d8d8d8;
+
+      &:first-child {
+        border-top: 0.1vw solid #d8d8d8;
+      }
+
+      .info {
+        .name {
+          font-size: 3.6vw;
+          color: #404040;
+          line-height: 1.5;
+          margin-bottom: 2.4vw;
+          @include ellipsis(2);
+          text-align: justify;
+        }
+        .date {
+          position: absolute;
+          left: 0;
+          bottom: 2.4vw;
+          font-size: 3.2vw;
+          color: #b2b2b2;
+          line-height: 1.4;
+        }
+
+        .download {
+          position: absolute;
+          right: 0;
+          bottom: 2.4vw;
+          font-size: 3.2vw;
+          @include themify($themes) {
+            color: themed('color');
+          }
+          cursor: pointer;
+          margin-top: 1.2vw;
+        }
+      }
+
+      .cover {
+        position: relative;
+        width: 92vw;
+        height: 56vw;
+        border-bottom: 0.1vw solid #d8d8d8;
+        background: #333;
+        span {
+          position: absolute;
+          width: 12vw;
+          height: 12vw;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          background: url(~assets/theme-images/common/h5-icon-play.png)
+            no-repeat center;
+          background-size: 12vw;
+          border-radius: 50%;
+          cursor: pointer;
+          box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
+        }
+      }
+    }
+  }
+}
+</style>

+ 264 - 0
pages/_template/ross/feedback/index.vue

@@ -0,0 +1,264 @@
+<template>
+  <div class="page">
+    <div class="page-top flex flex-col justify-center items-center">
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="name mt-2" v-text="supplierInfo.shopName + '意见反馈'"></div>
+    </div>
+    <div class="page-content 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">
+      <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 { mapGetters } from 'vuex'
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      content: '',
+      showModal: false,
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'userInfo', 'routePrefix']),
+    isEmpty() {
+      return this.content.length === 0
+    },
+  },
+  methods: {
+    async onSubmit() {
+      const { clubUserId } = this.userInfo
+      if (this.isEmpty) {
+        this.$toast('留言不能为空')
+        return
+      }
+      try {
+        await this.$http.api.feedback({ clubUserId, content: this.content })
+        this.showModal = true
+        this.content = ''
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    onConfirm() {
+      this.showModal = false
+      this.$router.push(this.routePrefix)
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+// pc 端
+@media screen and (min-width: 768px) {
+  .page-top {
+    height: 360px;
+    @include themify($themes) {
+      background: themed('pc-banner-feedback');
+    }
+    background-size: auto 360px;
+    .logo {
+      display: block;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: #fff;
+    }
+    .name {
+      font-size: 30px;
+      color: #fff;
+    }
+  }
+  .page-content {
+    width: 1200px;
+    margin-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;
+      @include themify($themes) {
+        background-color: themed('color');
+      }
+      color: #fff;
+      transition: all 0.2s;
+      cursor: pointer;
+
+      &:hover {
+        @include themify($themes) {
+          background-color: themed('hover-color');
+        }
+      }
+
+      &.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;
+        @include themify($themes) {
+          background: themed('pc-icon-feedback-submit') no-repeat center;
+          background-size: 140px 100px;
+        }
+      }
+    }
+  }
+}
+
+// 移动 端
+@media screen and (max-width: 768px) {
+  .page-top {
+    height: 46vw;
+    @include themify($themes) {
+      background: themed('h5-banner-feedback');
+    }
+    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 {
+    .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;
+      @include themify($themes) {
+        background-color: themed('color');
+      }
+      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: 20vw;
+        @include themify($themes) {
+          background: themed('pc-icon-feedback-submit') no-repeat center;
+          background-size: 30vw 20vw;
+        }
+      }
+    }
+  }
+}
+</style>

+ 531 - 0
pages/_template/ross/form/club-register.vue

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

+ 603 - 0
pages/_template/ross/form/components/form-club-device.vue

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

+ 829 - 0
pages/_template/ross/form/components/form-club-info.vue

@@ -0,0 +1,829 @@
+<template>
+  <div class="club-info">
+    <el-form :model="formData" :rules="rules" ref="form" label-position="left">
+      <el-form-item prop="name" label="机构名称:">
+        <el-input
+          placeholder="请输入机构名称"
+          v-model="formData.name"
+          maxlength="50"
+          show-word-limit
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="mobile" label="联系电话:">
+        <el-input
+          placeholder="请输入对外联系电话"
+          v-model="formData.mobile"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="address" label="所在地区:">
+        <br />
+        <input type="text" v-model="formData.address" v-show="false" />
+        <div class="flex items-center justify-between">
+          <el-select
+            placeholder="请选择"
+            v-model="formData.provinceId"
+            @change="onProvinceChange"
+          >
+            <template v-for="item in provinceList">
+              <el-option :label="item.name" :value="item.id" :key="item.id">
+              </el-option>
+            </template>
+          </el-select>
+          <el-select
+            placeholder="请选择"
+            v-model="formData.cityId"
+            @change="onCityChange"
+            class="mx-2"
+          >
+            <template v-for="item in cityList">
+              <el-option :label="item.name" :value="item.id" :key="item.id">
+              </el-option>
+            </template>
+          </el-select>
+          <el-select
+            placeholder="请选择"
+            v-model="formData.townId"
+            @change="onTownChange"
+          >
+            <template v-for="item in townList">
+              <el-option :label="item.name" :value="item.id" :key="item.id">
+              </el-option>
+            </template>
+          </el-select>
+        </div>
+        <el-input
+          class="mt-4"
+          type="textarea"
+          :rows="4"
+          v-model="formData.fullAddress"
+          @input="onFullAddressInput"
+          placeholder="建议您如实填写详细收货地址,例如:街道名称,门牌号码,楼层和房间号等信息"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="point" label="">
+        <div class="normal-row">
+          <div class="label">
+            <i>*</i>所在位置:<span
+              >(提示:打开地图,将定位图标移到具体位置)</span
+            >
+          </div>
+
+          <div class="postion-btn" @click="initMap">定位</div>
+        </div>
+        <el-input v-model="formData.point" disabled></el-input>
+      </el-form-item>
+      <el-form-item prop="logoImage" label="logo:">
+        <br />
+        <el-input v-show="false" v-model="formData.logoImage" />
+        <SimpleUploadImage
+          :limit="1"
+          :image-list="logoList"
+          :before-upload="beforeLogoUpload"
+          @success="uploadLogoSuccess"
+          @remove="handleLogoRemove"
+        />
+      </el-form-item>
+      <el-form-item prop="banner">
+        <div class="normal-row">
+          <div class="label"><i>*</i>门头照:<span>(可上传6张)</span></div>
+          <el-input v-show="false" v-model="formData.banner" />
+          <SimpleUploadImage
+            :limit="6"
+            :image-list="bannerList"
+            :before-upload="beforeBannerUpload"
+            @success="uploadBannerSuccess"
+            @remove="handleBannerRemove"
+          />
+        </div>
+      </el-form-item>
+
+      <el-form-item label="机构类型:" prop="firstClubType">
+        <!-- <el-radio-group v-model="formData.firstClubType">
+          <el-radio :label="1">医美</el-radio>
+          <el-radio :label="2">生美</el-radio>
+          <el-radio :label="3">项目公司</el-radio>
+          <el-radio :label="4">个人</el-radio>
+          <el-radio :label="5">其他</el-radio>
+        </el-radio-group> -->
+        <SimpleRadio v-model="formData.firstClubType" :list="clubTypeList" />
+      </el-form-item>
+
+      <el-form-item
+        v-if="formData.firstClubType === 1"
+        label="医美类型:"
+        prop="secondClubType"
+      >
+        <!-- <el-radio-group v-model="formData.secondClubType">
+          <el-radio :label="1">诊所</el-radio>
+          <el-radio :label="2">门诊</el-radio>
+          <el-radio :label="3">医院</el-radio>
+          <el-radio :label="4">其他</el-radio>
+        </el-radio-group> -->
+        <SimpleRadio
+          v-model="formData.secondClubType"
+          :list="medicalTypeList1"
+          type="rect"
+        />
+      </el-form-item>
+
+      <el-form-item
+        v-if="formData.firstClubType === 2"
+        label="生美类型:"
+        prop="secondClubType"
+      >
+        <!-- <el-radio-group v-model="formData.secondClubType">
+          <el-radio :label="5">美容院</el-radio>
+          <el-radio :label="6">养生馆</el-radio>
+          <el-radio :label="7">其他</el-radio>
+        </el-radio-group> -->
+        <SimpleRadio
+          v-model="formData.secondClubType"
+          :list="medicalTypeList2"
+          type="rect"
+        />
+      </el-form-item>
+
+      <el-form-item
+        label="医疗许可证:"
+        prop="medicalLicenseImage"
+        v-if="formData.firstClubType === 1"
+      >
+        <br />
+        <el-input v-show="false" v-model="formData.medicalLicenseImage" />
+        <SimpleUploadImage
+          :limit="1"
+          :image-list="licenseImageList"
+          :before-upload="beforeLicenseImageUpload"
+          @success="uploadLicenseImageSuccess"
+          @remove="handleLicenseImageRemove"
+        />
+      </el-form-item>
+
+      <el-form-item label="员工人数:" prop="empNum">
+        <el-input
+          v-model.number="formData.empNum"
+          placeholder="请输入员工人数"
+          clearable
+        />
+      </el-form-item>
+    </el-form>
+
+    <div class="position-select" v-if="mapVisiable">
+      <div class="position-select-container">
+        <SimpleAMap
+          @position="onPosition"
+          ref="aMap"
+          :lnglat="lnglat"
+          :address="fullAddress"
+        />
+        <div class="position-select-footer">
+          <div class="lnglat">当前经纬度:{{ lgnlatText }}</div>
+          <div
+            class="position-cancel postion-control"
+            @click="mapVisiable = false"
+          >
+            取消
+          </div>
+          <div
+            class="position-confirm postion-control"
+            @click="mapVisiable = false"
+          >
+            确定
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <SimpleDialog
+      v-model="active"
+      @confirm="active = false"
+      :cancel="false"
+      confirmText="好的"
+      description="请慎重填写机构信息,认证通过后将无法更改!"
+      :center="true"
+    />
+  </div>
+</template>
+
+<script>
+import SimpleUploadImage from '@/components/SimpleUploadImage'
+import SimpleRadio from '@/components/SimpleRadio'
+import { isPoint, isNumber } from '@/utils/validator'
+export default {
+  components: {
+    SimpleUploadImage,
+    SimpleRadio,
+  },
+  data() {
+    var validatePoint = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('经纬度坐标不能为空'))
+      } else {
+        if (isPoint(value)) {
+          callback()
+        } else {
+          callback(
+            new Error('经纬度坐标格式不正确,(例如:114.095294,22.536004)')
+          )
+        }
+      }
+    }
+
+    var validateMobile = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('联系方式不能为空'))
+      } else {
+        if (isNumber(value)) {
+          callback()
+        } else {
+          callback(new Error('联系方式格式不正确'))
+        }
+      }
+    }
+
+    var validateAddress = (rule, value, callback) => {
+      if (
+        !this.formData.provinceId ||
+        !this.formData.cityId ||
+        !this.formData.townId ||
+        !this.formData.fullAddress
+      ) {
+        callback(new Error('请输入完整的地址'))
+      } else {
+        callback()
+      }
+    }
+
+    return {
+      clubTypeList: [
+        { value: 1, name: '医美' },
+        { value: 2, name: '生美' },
+        { value: 3, name: '项目公司' },
+        { value: 4, name: '个人' },
+        { value: 5, name: '其他' },
+      ],
+      medicalTypeList1: [
+        { value: 1, name: '诊所' },
+        { value: 2, name: '门诊' },
+        { value: 3, name: '医院' },
+        { value: 4, name: '其他' },
+      ],
+      medicalTypeList2: [
+        { value: 5, name: '美容院' },
+        { value: 6, name: '养生馆' },
+        { value: 7, name: '其他' },
+      ],
+      active: true,
+      lnglat: null,
+      mapVisiable: false,
+      formData: {
+        name: '',
+        address: '',
+        fullAddress: '',
+        point: '',
+        mobile: '',
+        userMobile: '',
+        logoImage: '',
+        banner: '',
+        customFlag: 0,
+        remarks: '',
+        empNum: '',
+        firstClubType: 1,
+        secondClubType: 1,
+        medicalLicenseImage: '',
+        provinceId: '',
+        cityId: '',
+        townId: '',
+      },
+      rules: {
+        name: [
+          { required: true, message: '机构名称不能为空', trigger: ['blur'] },
+        ],
+        logoImage: [
+          { required: true, message: '请上传logo', trigger: ['change'] },
+        ],
+        mobile: [
+          {
+            required: true,
+            validator: validateMobile,
+            trigger: ['blur', 'change'],
+          },
+        ],
+        point: [
+          {
+            required: true,
+            validator: validatePoint,
+            trigger: ['change'],
+          },
+        ],
+        address: [
+          {
+            required: true,
+            message: '所在地区不能为空',
+            trigger: ['change'],
+          },
+          {
+            validator: validateAddress,
+            trigger: ['change'],
+          },
+        ],
+        banner: [
+          {
+            required: true,
+            message: '门头照不能为空',
+            trigger: ['change'],
+          },
+        ],
+        empNum: [
+          { required: true, message: '员工人数不能为空', trigger: ['blur'] },
+        ],
+        firstClubType: [
+          { required: true, message: '机构类型不能为空', trigger: ['change'] },
+        ],
+        secondClubType: [
+          {
+            required: true,
+            message: '医美类型/生美类型不能为空',
+            trigger: ['change'],
+          },
+        ],
+        medicalLicenseImage: [
+          {
+            required: true,
+            message: '医疗许可证不能为空',
+            trigger: ['change'],
+          },
+        ],
+      },
+      // logo图片列表
+      logoList: [],
+      // banner图片列表
+      bannerList: [],
+      // 级联选择的地址
+      address: '',
+      // 医疗许可证图片
+      licenseImageList: [],
+
+      provinceList: [],
+    }
+  },
+
+  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,
+        bannerList: this.bannerList.map((item) =>
+          item.response ? item.response.data : item.url
+        ),
+      }
+    },
+
+    async init(formData) {
+      this.formData.name = formData.authParty
+      this.formData.provinceId = formData.provinceId
+      this.formData.cityId = formData.cityId
+      this.formData.townId = formData.townId
+      this.formData.fullAddress = formData.address
+      this.formData.mobile = formData.mobile
+      this.formData.logoImage = formData.logo
+      this.formData.empNum = formData.empNum
+      this.formData.firstClubType = formData.firstClubType
+      this.formData.secondClubType = formData.secondClubType
+      this.formData.medicalLicenseImage = formData.medicalLicenseImage
+      this.formData.point = formData.lngAndLat
+      this.lnglat = formData.lngAndLat.split(',')
+      this.logoList = [{ name: '', url: formData.logo }]
+      this.bannerList = formData.bannerList.map((item) => ({
+        name: '',
+        url: item,
+      }))
+
+      if (formData.medicalLicenseImage) {
+        this.licenseImageList = [
+          { name: '', url: formData.medicalLicenseImage },
+        ]
+      }
+
+      this.countAddress()
+
+      this.formData.banner =
+        this.bannerList.length > 0 ? this.bannerList.length : ''
+    },
+
+    // 表单验证
+    validate() {
+      this.$emit('step', this.genetageFormData())
+      return this.$refs.form.validate()
+    },
+
+    onProvinceChange() {
+      this.formData.cityId = ''
+      this.formData.townId = ''
+      this.countAddress()
+    },
+    onCityChange() {
+      this.formData.townId = ''
+      this.countAddress()
+    },
+    onTownChange() {
+      this.countAddress()
+    },
+
+    onFullAddressInput() {
+      this.countAddress()
+    },
+
+    countAddress() {
+      this.formData.address =
+        this.formData.cityId +
+        this.formData.provinceId +
+        this.formData.townId +
+        this.formData.fullAddress
+    },
+
+    // logo上传
+    uploadLogoSuccess({ response, file, fileList }) {
+      this.logoList = fileList
+      this.formData.logoImage = fileList[0].response.data
+    },
+    handleLogoRemove({ file, fileList }) {
+      this.logoList = fileList
+      this.formData.logoImage = ''
+    },
+    beforeLogoUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('上传logo图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+
+    // banner上传
+    uploadBannerSuccess({ response, file, fileList }) {
+      this.bannerList = fileList
+      console.log(this.bannerList)
+      this.formData.banner = fileList.length > 0 ? fileList.length : ''
+    },
+    handleBannerRemove({ file, fileList }) {
+      this.bannerList = fileList
+      this.formData.banner = fileList.length > 0 ? fileList.length : ''
+    },
+    beforeBannerUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('上传banner图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+
+    // 医疗许可证图片上传
+    uploadLicenseImageSuccess({ response, file, fileList }) {
+      this.licenseImageList = fileList
+      console.log(this.licenseImageList)
+      this.formData.medicalLicenseImage = response.data
+    },
+    handleLicenseImageRemove({ file, fileList }) {
+      this.licenseImageList = fileList
+      this.formData.medicalLicenseImage = ''
+    },
+    beforeLicenseImageUpload(file) {
+      const flag = file.size / 1024 / 1024 < 5
+      if (!flag) {
+        this.$message.error('医疗许可证图片大小不能超过 5MB!')
+      }
+      return flag
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.club-info {
+  ::v-deep {
+    .el-input.is-active .el-input__inner,
+    .el-input__inner:focus {
+      @include themify($themes) {
+        border-color: themed('color');
+      }
+    }
+  }
+}
+
+// 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>

+ 230 - 0
pages/_template/ross/form/components/form-club-register.vue

@@ -0,0 +1,230 @@
+<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 {
+      @include themify($themes) {
+        border-color: themed('color');
+      }
+    }
+  }
+}
+
+.verifyCode {
+  .send {
+    cursor: pointer;
+    white-space: nowrap;
+    @include themify($themes) {
+      color: themed('color');
+    }
+  }
+}
+</style>

+ 271 - 0
pages/_template/ross/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-ross',
+  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>

+ 649 - 0
pages/_template/ross/index.vue

@@ -0,0 +1,649 @@
+<template>
+  <div class="page">
+    <van-list
+      v-model="isLoadingMore"
+      :finished="finished"
+      :immediate-check="false"
+      :finished-text="total ? '没有更多了' : ''"
+      @load="onLoadMore"
+    >
+      <div class="page-top flex flex-col justify-center items-center"></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.clubName"
+              @change="onSearch"
+            >
+              <i slot="prefix" class="el-input__icon el-icon-search"></i>
+            </el-input>
+          </div>
+          <div class="area">
+            <RossSelectGroup @change="onCityChange" ref="citySelect" />
+          </div>
+        </div>
+
+        <!-- 标题 -->
+        <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.clubName)" />
+              <div class="info">
+                <div class="name" v-text="item.clubName"></div>
+                <div class="mobile">{{ item.mobile || '未知' }}</div>
+                <div class="address">
+                  {{ formatAddress(item.area, item.address) }}
+                </div>
+                <div
+                  class="distance"
+                  v-text="item.distance + 'km'"
+                  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 { loactionSelf } from '@/utils/map-utils'
+import { drawLogo, debounce } from '@/utils'
+export default {
+  layout: 'app-ross',
+  data() {
+    return {
+      isLoadingMore: true,
+      finished: false,
+      isRequest: true,
+      list: [],
+      listQuery: {
+        authUserId: '',
+        lngAndLat: '',
+        clubName: '',
+        provinceId: '',
+        cityId: '',
+        townId: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      total: 0,
+      scrollTop: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['authUserId', 'routePrefix', 'screenWidth', 'isPc']),
+    offsetTop() {
+      if (this.scrollTop <= window.innerHeight / 2) return '240px'
+      return 240 + this.scrollTop - window.innerHeight / 2 + 'px'
+    },
+  },
+  mounted() {
+    const cacheData = this.$getStorage(this.routePrefix, 'club_list_data')
+    if (cacheData) {
+      this.initFromCache(cacheData)
+      this.$removeStorage(this.routePrefix, 'club_list_data')
+    } else {
+      this.initData()
+    }
+    window.addEventListener('scroll', () => {
+      this.scrollTop = document.documentElement.scrollTop
+    })
+  },
+  beforeDestroy() {
+    this.$toast.clear()
+    window.removeEventListener('scroll', () => {})
+  },
+  methods: {
+    // 绘制logo的方法
+    drawLogo,
+    // 查看详情
+    toDetail(item) {
+      this.$setStorage(this.routePrefix, 'club_list_data', this.$data, {
+        expiredTime: 5 * 60 * 1000,
+      })
+      this.$setStorage(this.routePrefix, 'clubInfo', item)
+      const url = `${this.routePrefix}/approve/club/detail?id=${item.authId}`
+      this.$router.push(url)
+    },
+    // 从缓存中获取数据
+    initFromCache(cacheData) {
+      this.isLoadingMore = cacheData.isLoadingMore
+      this.finished = cacheData.finished
+      this.isRequest = cacheData.isRequest
+      this.list = cacheData.list
+      this.listQuery = cacheData.listQuery
+      this.total = cacheData.total
+      this.$nextTick(() => {
+        this.$refs.citySelect.initSelectValue({
+          provinceId: this.listQuery.provinceId,
+          cityId: this.listQuery.cityId,
+          townId: this.listQuery.townId,
+        })
+      })
+    },
+    // 初始化页面数据
+    async initData() {
+      this.listQuery.authUserId = this.authUserId
+
+      // 自定义加载图标
+      this.$toast.loading({
+        message: '正在获取您附近的机构...',
+        duration: 0,
+      })
+
+      // 获取定位信息 百度坐标转高德坐标
+      try {
+        const location = await loactionSelf()
+        const result = await this.$http.api.assistant({
+          key: '1bcc97330f6cf517e8dd9d5278957e67',
+          locations: `${location.point.lng},${location.point.lat}`,
+          coordsys: 'baidu',
+          output: 'JSON',
+        })
+        const res = await result.json()
+        this.listQuery.lngAndLat = res.locations
+      } catch (error) {
+        this.$toast.clear()
+        this.$toast('获取定位信息失败,请确保您开启的定位权限并保存网络畅通')
+        this.isRequest = false
+      }
+
+      // 获取机构列表
+      this.fetchList()
+    },
+    fetchList: debounce(async function () {
+      try {
+        this.isLoadingMore = true
+        const res = await this.$http.api.getAuthClubList(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
+      }
+    }, 400),
+    // 城市变化
+    onCityChange(valueMap) {
+      const { provinceId, cityId, townId } = valueMap
+      this.listQuery.provinceId = provinceId
+      this.listQuery.cityId = cityId
+      this.listQuery.townId = townId
+
+      this.listQuery.pageNum = 1
+      this.list = []
+      this.fetchList()
+    },
+    // 搜索
+    onSearch() {
+      this.listQuery.pageNum = 1
+      this.list = []
+      this.fetchList()
+    },
+    // 格式化地址
+    formatAddress(a1, a2) {
+      let resutl = ''
+      if (typeof a1 === 'string') {
+        resutl += a1
+      }
+      if (typeof a2 === 'string') {
+        resutl += a2
+      }
+      return resutl || '未知'
+    },
+    // 加载更多
+    onLoadMore() {
+      this.fetchList()
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.el-input {
+  ::v-deep {
+    & > {
+      .el-input.is-active .el-input__inner,
+      .el-input__inner:focus {
+        @include themify($themes) {
+          border-color: themed('color');
+        }
+      }
+    }
+  }
+}
+
+// pc 端
+@media screen and (min-width: 768px) {
+  .page {
+    position: relative;
+    min-height: calc(100vh - 80px - 80px);
+    background-color: #fff;
+  }
+  .page-top {
+    height: 530px;
+    @include themify($themes) {
+      background-image: themed('pc-banner-club');
+    }
+    background-size: cover;
+    background-position: center;
+  }
+  .page-content {
+    position: relative;
+    width: 1000px;
+    margin: 0 auto;
+
+    .title {
+      font-size: 16px;
+      color: #404040;
+
+      span {
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .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/ross/pc-nav-entry-device-active.png)
+                  no-repeat center center,
+                linear-gradient(180deg, #ffba63 0%, #f3920d 100%);
+              background-size: 48px, 100%;
+            }
+            &.icon-doctor {
+              background: url(~assets/theme-images/ross/pc-nav-entry-doctor-active.png)
+                  no-repeat center center,
+                linear-gradient(180deg, #ffba63 0%, #f3920d 100%);
+              background-size: 48px, 100%;
+            }
+          }
+          .text {
+            @include themify($themes) {
+              color: themed('color');
+            }
+          }
+        }
+
+        span {
+          display: block;
+        }
+
+        .icon {
+          width: 72px;
+          height: 72px;
+          // background: linear-gradient(180deg, #ffba63 0%, #f3920d 100%);
+          background-color: #f6f6f7;
+          border-radius: 12px;
+
+          &.icon-device {
+            background: url(~assets/theme-images/ross/pc-nav-entry-device.png)
+              no-repeat center center #f6f6f7;
+            background-size: 48px;
+          }
+
+          &.icon-doctor {
+            background: url(~assets/theme-images/ross/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;
+    @include themify($themes) {
+      background: themed('h5-banner-club');
+    }
+    background-size: 100vw 100vw !important;
+
+    .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 {
+        @include themify($themes) {
+          color: themed('color');
+        }
+      }
+    }
+
+    .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%, #f3920d 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/ross/pc-nav-entry-device-active.png)
+              no-repeat center;
+          }
+        }
+
+        &.icon-doctor {
+          &::after {
+            background: url(~assets/theme-images/ross/pc-nav-entry-doctor-active.png)
+              no-repeat center;
+          }
+        }
+      }
+
+      .text {
+        font-size: 2.4vw;
+        color: #f3920d;
+        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>

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

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

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

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

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

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

+ 264 - 0
pages/_template/ross/record/device/edit.vue

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

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

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

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

@@ -0,0 +1,116 @@
+<template>
+  <div class="flex justify-center page">
+    <div class="page-content flex flex-col items-center">
+      <div class="icon-submit-succsss"></div>
+      <div class="tip mt-4 mb-6">提交成功</div>
+      <div class="label text-center">
+        审核通过后,用户可通过正品授权入口或扫二维码查看 机构及设备正品授权信息
+      </div>
+      <div class="record-btn mt-4" @click="toClubRecord">认证记录</div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  layout: 'app-ross',
+  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;
+      @include themify($themes) {
+        background: themed('color');
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .page-content {
+    padding: 0 9.2vw;
+    padding-top: 25.2vw;
+
+    .icon-submit-succsss {
+      width: 12vw;
+      height: 12vw;
+      background: url(~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;
+      @include themify($themes) {
+        background: themed('color');
+      }
+    }
+  }
+}
+</style>

+ 2 - 0
store/app.js

@@ -5,6 +5,7 @@ const state = () => ({
   loginVisiable: false,
   routePrefix: '', // 路由前缀
   themeName: 'normal',
+  screenWidth: 0,
 })
 
 const mutations = {
@@ -16,6 +17,7 @@ const mutations = {
       state.isPc = false
       state.screen = 'h5'
     }
+    state.screenWidth = width
     state.static = `${process.env.STATIC_URL}/${state.screen}`
   },
   SHOW_LOGIN(state) {

+ 1 - 0
store/getters.js

@@ -6,6 +6,7 @@ export default {
   routePrefix: (state) => state.app.routePrefix,
   loginVisiable: (state) => state.app.loginVisiable,
   themeName: (state) => state.app.themeName,
+  screenWidth: (state) => state.app.screenWidth,
   // 用户相关
   userInfo: (state) => state.user.userInfo,
   authUserId: (state) => state.user.authUserId,

+ 61 - 0
utils/donwload-tools.js

@@ -0,0 +1,61 @@
+import SimpleProgress from '@/components/SimpleProgress'
+import handleClipboard from '@/utils/clipboard'
+import { isWeChat } from '@/utils/validator'
+
+let uuid = 0 // 进度条id
+
+export async function downloadWithUrl(downUrl, fileName, self) {
+  const h = self.$createElement
+  let progressRef, tiemer
+
+  const notification = self.$notify({
+    title: '提示',
+    message: h('div', {}, [
+      h('div', {}, `正在下载${fileName},请勿重复操作!`),
+      h(SimpleProgress, { ref: `progress${uuid}` }),
+    ]),
+    duration: 0,
+  })
+
+  self.$nextTick(() => {
+    progressRef = self.$refs[`progress${uuid}`]
+    tiemer = setInterval(() => {
+      if (progressRef.percentage < 90) {
+        progressRef.percentage += 2
+      } else {
+        clearInterval(tiemer)
+      }
+    }, 500)
+  })
+
+  try {
+    const data = await fetch(downUrl)
+    const res = await data.blob()
+    const link = document.createElement('a')
+    const url = URL.createObjectURL(res)
+    link.href = url
+    link.download = fileName
+    link.click()
+    console.log(url)
+    URL.revokeObjectURL(url)
+  } catch (err) {
+    self.$message.error(`下载${fileName}失败`)
+  } finally {
+    clearInterval(tiemer)
+    progressRef.percentage = 100
+    notification && notification.close()
+  }
+}
+
+// 通过链接下载
+export default function downloadFile(downUrl, fileName, self, $event) {
+  if (isWeChat()) {
+    return handleClipboard(
+      downUrl,
+      $event,
+      '下载链接已复制到剪切板,请粘贴到浏览器中下载'
+    )
+  } else {
+    downloadWithUrl(downUrl, fileName, self)
+  }
+}

+ 27 - 0
utils/index.js

@@ -1,3 +1,5 @@
+import { Progress } from 'element-ui'
+
 // 绘制logo
 export function drawLogo(text = '', len = 4) {
   if (text.length > 4) {
@@ -128,3 +130,28 @@ export function downloadUrlLink(url) {
   a.click()
   document.body.removeChild(a)
 }
+
+// 下载方式2
+export async function downloadWithUrl(downUrl, fileName, self) {
+  const h = self.$createElement
+  const notification = self.$notify({
+    title: '提示',
+    message: h('div', {}, [
+      h('div', {}, `正在下载${fileName},请勿重复操作!`),
+      h(Progress, { props: { percentage: 100 } }),
+    ]),
+    duration: 0,
+  })
+  try {
+    const data = await fetch(downUrl)
+    const res = await data.blob()
+    const a = document.createElement('a')
+    a.href = URL.createObjectURL(res)
+    a.download = fileName
+    a.click()
+  } catch (err) {
+    self.$message.error(`下载${fileName}失败`)
+  } finally {
+    notification.close()
+  }
+}