Browse Source

页面重构之接口调用

yuwenjun1997 3 years ago
parent
commit
a214fb4709
52 changed files with 3462 additions and 472 deletions
  1. 4 1
      .env.development
  2. 4 1
      .env.production
  3. 91 0
      apis/index.js
  4. 345 0
      components/LdmCity/index.vue
  5. 77 6
      components/LdmLogin/index.vue
  6. 349 0
      components/SimpleCity/index.vue
  7. 7 2
      components/SimpleEmpty/index.vue
  8. 76 5
      components/SimpleLogin/index.vue
  9. 6 2
      components/SimplePagination/index.vue
  10. 93 29
      components/SimpleSearch/index.vue
  11. 57 0
      components/SimpleSwiper/index.vue
  12. 24 6
      components/SimpleTabs/index.vue
  13. 27 0
      configs/tabs.js
  14. 10 0
      keys.config.js
  15. 76 2
      layouts/app-ldm.vue
  16. 67 6
      layouts/app.vue
  17. 70 0
      package-lock.json
  18. 5 0
      package.json
  19. 75 24
      pages/ldm/approve/club/detail.vue
  20. 179 19
      pages/ldm/approve/club/index.vue
  21. 6 7
      pages/ldm/approve/index.vue
  22. 47 14
      pages/ldm/approve/personnel/operate/detail.vue
  23. 47 14
      pages/ldm/approve/personnel/training/detail.vue
  24. 71 21
      pages/ldm/approve/personnel/training/index.vue
  25. 68 29
      pages/ldm/database/package.vue
  26. 7 1
      pages/ldm/index.vue
  27. 85 39
      pages/ph/approve/club/detail.vue
  28. 198 41
      pages/ph/approve/club/index.vue
  29. 103 19
      pages/ph/approve/device/_id.vue
  30. 97 26
      pages/ph/approve/device/index.vue
  31. 6 2
      pages/ph/approve/index.vue
  32. 59 28
      pages/ph/approve/personnel/operate/detail.vue
  33. 132 26
      pages/ph/approve/personnel/operate/index.vue
  34. 40 6
      pages/ph/database/article-detail.vue
  35. 90 31
      pages/ph/database/article.vue
  36. 104 11
      pages/ph/database/file.vue
  37. 104 26
      pages/ph/database/image.vue
  38. 91 9
      pages/ph/database/package.vue
  39. 92 12
      pages/ph/database/video.vue
  40. 126 3
      pages/ph/feedback/index.vue
  41. 12 2
      pages/ph/index.vue
  42. 20 2
      plugins/axios.js
  43. 2 0
      plugins/vant.js
  44. 7 0
      store/getters.js
  45. 18 0
      store/supplier.js
  46. 49 0
      store/user.js
  47. 13 0
      utils/auth.js
  48. 32 0
      utils/clipboard.js
  49. 12 0
      utils/download-link.js
  50. 1 0
      utils/index.js
  51. 66 0
      utils/map-utils.js
  52. 15 0
      utils/validator.js

+ 4 - 1
.env.development

@@ -5,4 +5,7 @@ EVN = 'development'
 BASE_URL = 'https://zplma-b.caimei365.com'
 
 # 静态资源文件地址
-STATIC_URL = 'https://static.caimei365.com/www/authentic'
+STATIC_URL = 'https://static.caimei365.com/www/authentic'
+
+# 采美网
+CIMEI_LOCAL = 'https://www-b.caimei365.com'

+ 4 - 1
.env.production

@@ -5,4 +5,7 @@ EVN = 'production'
 BASE_URL = 'https://zplma.caimei365.com'
 
 # 静态资源文件地址
-STATIC_URL = 'https://static.caimei365.com/www/authentic'
+STATIC_URL = 'https://static.caimei365.com/www/authentic'
+
+# 采美网
+CIMEI_LOCAL = 'https://www.caimei365.com'

+ 91 - 0
apis/index.js

@@ -0,0 +1,91 @@
+/*
+ * queryStringify
+ * 将k-v的对象序列化转成 url?k=v&k1=v1;
+ */
+const queryStringify = function (search = {}) {
+  return Object.entries(search)
+    .reduce((t, v) => `${t}${v[0]}=${encodeURIComponent(v[1])}&`, Object.keys(search).length ? '?' : '')
+    .replace(/&$/, '')
+}
+
+export default ($axios) => {
+  // 订阅号用户登录
+  const customLogin = (data) => $axios.post('/wx/user/login/subscribe/verify/code', data)
+  // 订阅号用户绑定邀请码登录
+  const customLoginWithCode = (data) => $axios.post('/wx/user/login/subscribe/invitation/code', data)
+  // 服务号微信授权登录
+  const wechatLogin = (data) => $axios.post('/wx/user/login/authorization', data)
+  // 服务号微信授权绑定邀请码登录
+  const wechatLoginWithCode = (data) => $axios.post('/wx/user/login/service/invitation/code', data)
+  // 获取jssdk配置参数
+  const initWxConfig = (params = {}) => $axios.get('/wx/sdk/config/data', { params })
+  // 发送验证码
+  const sendVerifyCode = (data = {}) => $axios.post('/wx/user/verify/code/send', data)
+  // 获取文章列表
+  const getArticleList = (params = {}) => $axios.get('/wx/data/article/list', { params })
+  // 获取图片列表
+  const getImageList = (params = {}) => $axios.get('/wx/data/image/list', { params })
+  // 获取视频列表
+  const getVideoList = (params = {}) => $axios.get('/wx/data/video/list', { params })
+  // 获取文件列表
+  const getFileList = (params = {}) => $axios.get('/wx/data/file/list', { params })
+  // 获取文章详情
+  const getArticleDetail = (params = {}) => $axios.get('/wx/data/article/form/data', { params })
+  // 用户反馈
+  const feedback = (data = {}) => $axios.post('/wx/data/feedback/submit', data)
+  // 获取已认证机构列表
+  const getAuthClubList = (params = {}) => $axios.get('/wx/auth/club/list', { params })
+  // 获取已认证机构详情
+  const getAuthClubDetail = (params = {}) => $axios.get('/wx/auth/club/details', { params })
+  // 获取已认证商品分类
+  const getAuthProductCateList = (params = {}) => $axios.get('/wx/auth/product/type/list', { params })
+  // 获取已认证商品列表
+  const getAuthProductList = (params = {}) => $axios.get('/wx/auth/product/list', { params })
+  // 验证token是否到期
+  const checkToken = (data = {}) => $axios.post('/wx/user/token/check', data)
+  // 下载文件
+  const downFile = (params = {}) => $axios.get('/download/file', { params })
+  // 获取城市列表
+  const fetchCityList = (params = {}) => $axios.get('/address/select', { params })
+  // 获取城市列表
+  const fetchAllCityList = () => $axios.get('/address/select/all')
+  // 获取供应商信息
+  const fetchSupplierInfo = (params = {}) => $axios.get('/wx/auth/shop/info', { params })
+  // 获取医师列表
+  const fetchDoctorList = (params = {}) => $axios.get('/wx/auth/doctor/list', { params })
+  // 获取医师详情
+  const fetchDoctorDetail = (params = {}) => $axios.get('/wx/auth/doctor/details', { params })
+  // 公众号类型
+  const checkAccountType = (params = {}) => $axios.get('/wx/sdk/account/type', { params })
+  // 高德地图api : 将坐标转化为高德地图坐标
+  const assistant = (params = {}) =>
+    fetch('https://restapi.amap.com/v3/assistant/coordinate/convert' + queryStringify(params))
+
+  return {
+    customLogin,
+    customLoginWithCode,
+    wechatLogin,
+    wechatLoginWithCode,
+    initWxConfig,
+    sendVerifyCode,
+    getArticleList,
+    getImageList,
+    getVideoList,
+    getFileList,
+    getArticleDetail,
+    feedback,
+    getAuthClubList,
+    getAuthProductCateList,
+    getAuthProductList,
+    checkToken,
+    downFile,
+    fetchCityList,
+    fetchAllCityList,
+    fetchSupplierInfo,
+    getAuthClubDetail,
+    fetchDoctorList,
+    fetchDoctorDetail,
+    checkAccountType,
+    assistant
+  }
+}

+ 345 - 0
components/LdmCity/index.vue

@@ -0,0 +1,345 @@
+<template>
+  <div class="simple-city" id="simple-city">
+    <div class="select-group">
+      <div
+        class="select"
+        v-for="(item, index) in placeholderValue"
+        :key="index"
+        @click.stop="onClick(item, index)"
+      >
+        <span
+          v-if="selectedValue[index]"
+          v-text="selectedValue[index][labelName]"
+        ></span>
+        <span v-text="item.label" v-else></span>
+      </div>
+    </div>
+    <keep-alive>
+      <div class="option-group">
+        <div class="options" v-show="showOptions" :class="[translateX]">
+          <div class="scroll-box">
+            <div class="option" @click="onOptionsClick(emptyData)">全部</div>
+            <div
+              class="option"
+              v-for="item in list"
+              :key="item[valueName]"
+              @click="onOptionsClick(item)"
+              v-text="item[labelName]"
+            ></div>
+          </div>
+        </div>
+      </div>
+    </keep-alive>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    labelName: {
+      type: String,
+      default: 'label',
+    },
+    valueName: {
+      type: String,
+      default: 'value',
+    },
+    options: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      selected: 0,
+      showOptions: false,
+      offset: 0,
+      current: 0,
+      emptyData: {
+        label: '全部',
+        value: '',
+      },
+      placeholderValue: [
+        {
+          label: '省份',
+        },
+        {
+          label: '城市',
+        },
+        {
+          label: '区县',
+        },
+      ],
+      selectedValue: [],
+      list: [],
+    }
+  },
+  computed: {
+    translateX() {
+      return 'offset-' + this.current
+    },
+  },
+  beforeDestroy() {
+    window.removeEventListener('click', () => {})
+  },
+  mounted() {
+    window.addEventListener('click', ($event) => {
+      this.showOptions = false
+    })
+  },
+  methods: {
+    onClick(item, index) {
+      if (index === 0 || this.selectedValue[index - 1]) {
+        this.offset = item.offset
+        this.current = index
+        this.filterList()
+        this.showOptions = true
+      } else {
+        this.showOptions = false
+      }
+    },
+    onOptionsClick(item) {
+      const valueName = this.valueName
+      if (this.current === 0) {
+        this.selectedValue = []
+      } else if (this.current === 1) {
+        this.$set(this.selectedValue, 2, undefined)
+      }
+      if (typeof item[valueName] === 'number' || item[valueName]) {
+        this.$set(this.selectedValue, this.current, item)
+      } else {
+        this.$set(this.selectedValue, this.current, undefined)
+      }
+      this.selectedValue = this.selectedValue.filter((item) => item)
+      this.$emit('change', this.selectedValue)
+    },
+    filterList() {
+      if (this.current === 0) {
+        this.list = this.options
+        return
+      }
+      const parent = this.selectedValue[this.current - 1]
+      this.list = parent.children
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+@media screen and (min-width: 768px) {
+  .simple-city {
+    .select-group {
+      .select {
+        display: inline-block;
+        position: relative;
+        margin-right: 48px;
+        height: 40px;
+        border-radius: 4px;
+        line-height: 40px;
+        box-sizing: border-box;
+        padding: 0 14px;
+        padding-right: 40px;
+        color: #000;
+        font-size: 21px;
+        cursor: pointer;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+
+        &::after {
+          position: absolute;
+          top: 50%;
+          right: 14px;
+          transform: translateY(-50%);
+          content: '';
+          display: block;
+          width: 0;
+          height: 0;
+          border-style: solid;
+          border-width: 10px 7px 0 7px;
+          border-color: #bababa transparent transparent transparent;
+          opacity: 0.8;
+        }
+      }
+    }
+
+    .option-group {
+      margin-top: 4px;
+      position: relative;
+      width: 100%;
+
+      .options {
+        display: inline-block;
+        min-width: 160px;
+        max-width: 400px;
+        max-height: 240px;
+        position: absolute;
+        padding-top: 4px;
+        border-radius: 4px;
+        box-shadow: 0 8px 8px rgba(0, 0, 0, 0.1);
+
+        &.offset-0 {
+          transform: translateX(0);
+        }
+        &.offset-1 {
+          transform: translateX(200px);
+        }
+        &.offset-2 {
+          transform: translateX(400px);
+        }
+
+        .scroll-box {
+          max-height: 240px;
+          overflow-y: scroll;
+          padding: 8px 0;
+          background: #fff;
+          border-radius: 4px;
+
+          &::-webkit-scrollbar {
+            width: 4px;
+            height: 4px;
+            background-color: #eee;
+          }
+
+          &::-webkit-scrollbar-thumb {
+            border-radius: 2px;
+            background-color: #ccc;
+          }
+        }
+
+        &::after {
+          position: absolute;
+          top: 0;
+          left: 14px;
+          transform: translateY(-50%);
+          content: '';
+          display: block;
+          width: 0;
+          height: 0;
+          border-style: solid;
+          border-width: 0 7px 10px 7px;
+          border-color: transparent transparent white transparent;
+        }
+
+        .option {
+          font-size: 16px;
+          background: #fff;
+          padding: 0 14px;
+          line-height: 40px;
+          color: #626262;
+          box-sizing: border-box;
+          cursor: pointer;
+          text-overflow: ellipsis;
+          overflow: hidden;
+          white-space: nowrap;
+
+          &:hover {
+            background: #f6f6f6;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .simple-city {
+    .select-group {
+      width: 92vw;
+      margin: 0 auto;
+      display: flex;
+      align-items: center;
+      .select {
+        position: relative;
+        height: 8vw;
+        line-height: 8vw;
+        box-sizing: border-box;
+        padding: 0 1.8vw;
+        padding-right: 6vw;
+        color: #000;
+        cursor: pointer;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+        border-radius: 1vw;
+        font-size: 3.5vw;
+        margin-right: 8.4vw;
+        &:last-child {
+          margin-right: 0;
+        }
+
+        &::after {
+          position: absolute;
+          top: 50%;
+          right: 1.8vw;
+          transform: translateY(-50%);
+          content: '';
+          display: block;
+          width: 0;
+          height: 0;
+          border-style: solid;
+          border-width: 1.8vw 1.2vw 0 1.2vw;
+          border-color: #474747 transparent transparent transparent;
+          opacity: 0.8;
+        }
+      }
+    }
+
+    .option-group {
+      position: relative;
+      width: 100%;
+
+      .options {
+        position: absolute;
+        top: 1.2vw;
+        display: inline-block;
+        min-width: 28vw;
+        max-width: 60vw;
+
+        &.offset-0 {
+          left: 4vw;
+        }
+        &.offset-1 {
+          transform: translateX(36vw);
+        }
+        &.offset-2 {
+          right: 4vw;
+        }
+
+        .scroll-box {
+          max-height: 50vw;
+          overflow-y: scroll;
+          background: #fff;
+          border-radius: 1.2vw;
+          border-radius: 1.2vw;
+          border: 0.1vw solid #ddd;
+
+          &::-webkit-scrollbar {
+            width: 0.2vw;
+            height: 0.2vw;
+            background-color: #eee;
+          }
+
+          &::-webkit-scrollbar-thumb {
+            border-radius: 0.1vw;
+            background-color: #ccc;
+          }
+        }
+
+        .option {
+          font-size: 3.2vw;
+          background: #fff;
+          padding: 0 1.2vw;
+          line-height: 7vw;
+          color: #626262;
+          box-sizing: border-box;
+          cursor: pointer;
+          text-overflow: ellipsis;
+          overflow: hidden;
+          white-space: nowrap;
+        }
+      }
+    }
+  }
+}
+</style>

+ 77 - 6
components/LdmLogin/index.vue

@@ -11,11 +11,20 @@
           />
           <div class="forlogoutm">
             <div class="form-item mb-4">
-              <input type="text" placeholder="手机号" />
+              <input
+                type="text"
+                placeholder="手机号"
+                v-model="formData.mobile"
+              />
             </div>
             <div class="form-item mb-4">
-              <input type="text" placeholder="验证码" class="code" />
-              <span class="send">获取验证码</span>
+              <input
+                type="text"
+                placeholder="验证码"
+                class="code"
+                v-model="formData.verifyCode"
+              />
+              <span class="send" @click="onSend">{{ sendCodeBtnText }}</span>
             </div>
             <div class="submit" @click="onSubmit">登录</div>
           </div>
@@ -26,17 +35,79 @@
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { isMobile } from '@/utils/validator'
 export default {
   name: 'ldm-login',
   data() {
     return {
-      show: true,
+      show: false,
+      sendStatus: 0,
+      formData: {
+        appId: '',
+        mobile: '',
+        verifyCode: '',
+      },
+      timer: null,
     }
   },
+  computed: {
+    ...mapGetters(['loginVisiable', 'appId']),
+    sendCodeBtnText() {
+      return this.sendStatus === 0
+        ? '发送验证码'
+        : `再次发送${this.sendStatus}s`
+    },
+  },
+  watch: {
+    loginVisiable() {
+      this.show = this.loginVisiable
+    },
+  },
   methods: {
-    onSubmit() {},
+    async onSubmit() {
+      try {
+        this.formData.appId = this.appId
+        const res = await this.$http.api.customLogin(this.formData)
+        this.$store.dispatch('user/login', res.data)
+        this.$store.commit('app/HIDE_LOGIN')
+      } catch (error) {
+        console.log(error)
+      }
+    },
     onClose() {
-      this.show = false
+      this.$store.commit('app/HIDE_LOGIN')
+    },
+    async onSend() {
+      if (this.sendStatus > 0) return
+      // 验证手机号是否合法
+      if (!isMobile(this.formData.mobile)) {
+        this.$toast('请输入正确的手机号')
+        return
+      }
+      try {
+        // 发送验证码
+        const res = await this.$http.api.sendVerifyCode({
+          mobile: this.formData.mobile,
+          appId: this.appId,
+          type: 1,
+        })
+        this.$toast('验证码已发送')
+        // 开启倒计时
+        this.countdown()
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    countdown() {
+      this.sendStatus = 30
+      this.timer = setInterval(() => {
+        if (this.sendStatus === 0) {
+          clearInterval(this.timer)
+          return
+        }
+        this.sendStatus--
+      }, 1000)
     },
   },
 }

+ 349 - 0
components/SimpleCity/index.vue

@@ -0,0 +1,349 @@
+<template>
+  <div class="simple-city" id="simple-city">
+    <div class="select-group">
+      <div
+        class="select"
+        v-for="(item, index) in placeholderValue"
+        :key="index"
+        @click.stop="onClick(item, index)"
+      >
+        <span
+          v-if="selectedValue[index]"
+          v-text="selectedValue[index][labelName]"
+        ></span>
+        <span v-text="item.label" v-else></span>
+      </div>
+    </div>
+    <keep-alive>
+      <div class="option-group">
+        <div class="options" v-show="showOptions" :class="[translateX]">
+          <div class="scroll-box">
+            <div class="option" @click="onOptionsClick(emptyData)">全部</div>
+            <div
+              class="option"
+              v-for="item in list"
+              :key="item[valueName]"
+              @click="onOptionsClick(item)"
+              v-text="item[labelName]"
+            ></div>
+          </div>
+        </div>
+      </div>
+    </keep-alive>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    labelName: {
+      type: String,
+      default: 'label',
+    },
+    valueName: {
+      type: String,
+      default: 'value',
+    },
+    options: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      selected: 0,
+      showOptions: false,
+      offset: 0,
+      current: 0,
+      emptyData: {
+        label: '全部',
+        value: '',
+      },
+      placeholderValue: [
+        {
+          label: '省',
+        },
+        {
+          label: '市',
+        },
+        {
+          label: '区',
+        },
+      ],
+      selectedValue: [],
+      list: [],
+    }
+  },
+  computed: {
+    translateX() {
+      return 'offset-' + this.current
+    },
+  },
+  beforeDestroy() {
+    window.removeEventListener('click', () => {})
+  },
+  mounted() {
+    window.addEventListener('click', ($event) => {
+      this.showOptions = false
+    })
+  },
+  methods: {
+    onClick(item, index) {
+      if (index === 0 || this.selectedValue[index - 1]) {
+        this.offset = item.offset
+        this.current = index
+        this.filterList()
+        this.showOptions = true
+      } else {
+        this.showOptions = false
+      }
+    },
+    onOptionsClick(item) {
+      const valueName = this.valueName
+      if (this.current === 0) {
+        this.selectedValue = []
+      } else if (this.current === 1) {
+        this.$set(this.selectedValue, 2, undefined)
+      }
+      if (typeof item[valueName] === 'number' || item[valueName]) {
+        this.$set(this.selectedValue, this.current, item)
+      } else {
+        this.$set(this.selectedValue, this.current, undefined)
+      }
+      this.selectedValue = this.selectedValue.filter((item) => item)
+      this.$emit('change', this.selectedValue)
+    },
+    filterList() {
+      if (this.current === 0) {
+        this.list = this.options
+        return
+      }
+      const parent = this.selectedValue[this.current - 1]
+      this.list = parent.children
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+@media screen and (min-width: 768px) {
+  .simple-city {
+    width: 560px;
+    .select-group {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      .select {
+        position: relative;
+        width: 160px;
+        height: 40px;
+        background: rgba(255, 255, 255, 0.39);
+        border-radius: 4px;
+        line-height: 40px;
+        box-sizing: border-box;
+        padding: 0 14px;
+        padding-right: 40px;
+        color: #fff;
+        cursor: pointer;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+
+        &::after {
+          position: absolute;
+          top: 50%;
+          right: 14px;
+          transform: translateY(-50%);
+          content: '';
+          display: block;
+          width: 0;
+          height: 0;
+          border-style: solid;
+          border-width: 10px 7px 0 7px;
+          border-color: white transparent transparent transparent;
+          opacity: 0.8;
+        }
+      }
+    }
+
+    .option-group {
+      margin-top: 4px;
+      position: relative;
+      width: 100%;
+
+      .options {
+        display: inline-block;
+        min-width: 160px;
+        max-width: 400px;
+        max-height: 240px;
+        position: relative;
+        padding-top: 4px;
+        border-radius: 4px;
+        box-shadow: 0 8px 8px rgba(0, 0, 0, 0.1);
+
+        &.offset-0 {
+          transform: translateX(0);
+        }
+        &.offset-1 {
+          transform: translateX(200px);
+        }
+        &.offset-2 {
+          transform: translateX(400px);
+        }
+
+        .scroll-box {
+          max-height: 240px;
+          overflow-y: scroll;
+          padding: 8px 0;
+          background: #fff;
+          border-radius: 4px;
+
+          &::-webkit-scrollbar {
+            width: 4px;
+            height: 4px;
+            background-color: #eee;
+          }
+
+          &::-webkit-scrollbar-thumb {
+            border-radius: 2px;
+            background-color: #ccc;
+          }
+        }
+
+        &::after {
+          position: absolute;
+          top: 0;
+          left: 14px;
+          transform: translateY(-50%);
+          content: '';
+          display: block;
+          width: 0;
+          height: 0;
+          border-style: solid;
+          border-width: 0 7px 10px 7px;
+          border-color: transparent transparent white transparent;
+        }
+
+        .option {
+          font-size: 16px;
+          background: #fff;
+          padding: 0 14px;
+          line-height: 40px;
+          color: #626262;
+          box-sizing: border-box;
+          cursor: pointer;
+          text-overflow: ellipsis;
+          overflow: hidden;
+          white-space: nowrap;
+
+          &:hover {
+            background: #f6f6f6;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .simple-city {
+    .select-group {
+      width: 92vw;
+      margin: 0 auto;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      .select {
+        position: relative;
+        width: 28vw;
+        height: 8vw;
+        background: rgba(255, 255, 255, 0.39);
+        line-height: 8vw;
+        box-sizing: border-box;
+        padding: 0 1.8vw;
+        padding-right: 6vw;
+        color: #000;
+        cursor: pointer;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+        border: 0.1vw solid #ddd;
+        border-radius: 1vw;
+        font-size: 3.2vw;
+
+        &::after {
+          position: absolute;
+          top: 50%;
+          right: 1.8vw;
+          transform: translateY(-50%);
+          content: '';
+          display: block;
+          width: 0;
+          height: 0;
+          border-style: solid;
+          border-width: 1.8vw 1.2vw 0 1.2vw;
+          border-color: #474747 transparent transparent transparent;
+          opacity: 0.8;
+        }
+      }
+    }
+
+    .option-group {
+      position: relative;
+      width: 100%;
+
+      .options {
+        position: absolute;
+        top: 1.2vw;
+        display: inline-block;
+        min-width: 28vw;
+        max-width: 60vw;
+
+        &.offset-0 {
+          left: 4vw;
+        }
+        &.offset-1 {
+          transform: translateX(36vw);
+        }
+        &.offset-2 {
+          right: 4vw;
+        }
+
+        .scroll-box {
+          max-height: 50vw;
+          overflow-y: scroll;
+          background: #fff;
+          border-radius: 1.2vw;
+          border-radius: 1.2vw;
+          border: 0.1vw solid #ddd;
+
+          &::-webkit-scrollbar {
+            width: 0.2vw;
+            height: 0.2vw;
+            background-color: #eee;
+          }
+
+          &::-webkit-scrollbar-thumb {
+            border-radius: 0.1vw;
+            background-color: #ccc;
+          }
+        }
+
+        .option {
+          font-size: 3.2vw;
+          background: #fff;
+          padding: 0 1.2vw;
+          line-height: 7vw;
+          color: #626262;
+          box-sizing: border-box;
+          cursor: pointer;
+          text-overflow: ellipsis;
+          overflow: hidden;
+          white-space: nowrap;
+        }
+      }
+    }
+  }
+}
+</style>

+ 7 - 2
components/SimpleEmpty/index.vue

@@ -1,5 +1,8 @@
 <template>
-  <van-empty :image="imageSrc" :description="description" />
+  <div>
+    <van-empty :image="imageSrc" :description="description" v-if="imageSrc" />
+    <van-empty :description="description" v-else />
+  </div>
 </template>
 
 <script>
@@ -24,7 +27,9 @@ export default {
   computed: {
     ...mapGetters(['static']),
     imageSrc() {
-      return this.image || this.static + '/' + this.name
+      if (this.image) return this.image
+      if (this.name) return this.static + '/' + this.name
+      return ''
     },
   },
 }

+ 76 - 5
components/SimpleLogin/index.vue

@@ -7,11 +7,20 @@
           <div class="title pb-6">登录</div>
           <div class="forlogoutm">
             <div class="form-item mb-4">
-              <input type="text" placeholder="手机号" />
+              <input
+                type="text"
+                placeholder="手机号"
+                v-model="formData.mobile"
+              />
             </div>
             <div class="form-item mb-4 code">
-              <input type="text" placeholder="验证码" class="code" />
-              <span class="send">获取验证码</span>
+              <input
+                type="text"
+                placeholder="验证码"
+                class="code"
+                v-model="formData.verifyCode"
+              />
+              <span class="send" @click="onSend">{{ sendCodeBtnText }}</span>
             </div>
             <div class="submit" @click="onSubmit">登录</div>
           </div>
@@ -22,17 +31,79 @@
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { isMobile } from '@/utils/validator'
 export default {
   name: 'simple-login',
   data() {
     return {
       show: false,
+      sendStatus: 0,
+      formData: {
+        appId: '',
+        mobile: '',
+        verifyCode: '',
+      },
+      timer: null,
     }
   },
+  computed: {
+    ...mapGetters(['loginVisiable', 'appId']),
+    sendCodeBtnText() {
+      return this.sendStatus === 0
+        ? '发送验证码'
+        : `再次发送${this.sendStatus}s`
+    },
+  },
+  watch: {
+    loginVisiable() {
+      this.show = this.loginVisiable
+    },
+  },
   methods: {
-    onSubmit() {},
+    async onSubmit() {
+      try {
+        this.formData.appId = this.appId
+        const res = await this.$http.api.customLogin(this.formData)
+        this.$store.dispatch('user/login', res.data)
+        this.$store.commit('app/HIDE_LOGIN')
+      } catch (error) {
+        console.log(error)
+      }
+    },
     onClose() {
-      this.show = false
+      this.$store.commit('app/HIDE_LOGIN')
+    },
+    async onSend() {
+      if (this.sendStatus > 0) return
+      // 验证手机号是否合法
+      if (!isMobile(this.formData.mobile)) {
+        this.$toast('请输入正确的手机号')
+        return
+      }
+      try {
+        // 发送验证码
+        const res = await this.$http.api.sendVerifyCode({
+          mobile: this.formData.mobile,
+          appId: this.appId,
+          type: 1,
+        })
+        this.$toast('验证码已发送')
+        // 开启倒计时
+        this.countdown()
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    countdown() {
+      this.sendStatus = 30
+      this.timer = setInterval(() => {
+        if (this.sendStatus === 0) {
+          clearInterval(this.timer)
+          return
+        }
+        this.sendStatus--
+      }, 1000)
     },
   },
 }

+ 6 - 2
components/SimplePagination/index.vue

@@ -3,7 +3,7 @@
     <van-pagination
       v-model="currentPage"
       :total-items="total"
-      :items-per-page="10"
+      :items-per-page="pageItems"
       :show-page-size="6"
       :force-ellipses="true"
       :mode="mode"
@@ -18,7 +18,11 @@ export default {
   props: {
     total: {
       type: Number,
-      default: 1000,
+      default: 0,
+    },
+    pageItems: {
+      type: Number,
+      default: 10,
     },
   },
   data() {

+ 93 - 29
components/SimpleSearch/index.vue

@@ -1,41 +1,105 @@
 <template>
-  <div class="simple-search"><input type="text" /></div>
+  <div class="simple-search">
+    <input
+      type="text"
+      :placeholder="placeholder"
+      v-model="inputVal"
+      @input="(e) => $emit('input', e.target.value)"
+      @keyup.enter="$emit('search', inputVal)"
+    />
+  </div>
 </template>
 
 <script>
-export default {}
+export default {
+  model: {
+    prop: 'value',
+    event: 'input',
+  },
+  props: {
+    value: {
+      type: String,
+      default: '',
+    },
+    placeholder: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      inputVal: '',
+    }
+  },
+}
 </script>
 
 <style scoped lang="scss">
-.simple-search {
-  width: 92vw;
-  height: 9.6vw;
-  box-shadow: 0px 0.4vw 1vw rgba(51, 51, 51, 0.08);
-  padding-left: 9.6vw;
-  padding-right: 1.6vw;
-  left: 50%;
-  top: 0;
-  background-color: #fff;
-  border-radius: 0.4vw;
-  &::before {
-    content: '';
-    display: block;
-    background: url(https://static.caimei365.com/www/authentic/h5/icon-search.png)
-      no-repeat;
-    width: 6.4vw;
-    height: 6.4vw;
-    position: absolute;
-    background-size: 6.4vw;
-    left: 1.6vw;
-    top: 1.6vw;
-  }
+@media screen and (min-width: 768px) {
+  .simple-search {
+    &::before {
+      content: '';
+      display: block;
+      background: url(https://static.caimei365.com/www/authentic/h5/icon-search.png)
+        no-repeat center;
+      width: 30px;
+      height: 30px;
+      position: absolute;
+      background-size: 26px;
+      top: 50%;
+      left: 6px;
+      transform: translateY(-50%);
+      z-index: 9;
+    }
 
-  input {
-    display: block;
-    width: 100%;
+    input {
+      position: relative;
+      display: block;
+      width: 560px;
+      height: 40px;
+      background: rgba(255, 255, 255, 0.39);
+      border-radius: 4px;
+      outline: none;
+      font-size: 16px;
+      line-height: 40px;
+      border: 0;
+      padding-left: 44px;
+      box-sizing: border-box;
+      color: #fff;
+    }
+  }
+}
+@media screen and (max-width: 768px) {
+  .simple-search {
+    width: 92vw;
     height: 9.6vw;
-    line-height: 9.6vw;
-    outline: none;
+    box-shadow: 0px 0.4vw 1vw rgba(51, 51, 51, 0.08);
+    padding-left: 9.6vw;
+    padding-right: 1.6vw;
+    left: 50%;
+    top: 0;
+    background-color: #fff;
+    border-radius: 0.4vw;
+    &::before {
+      content: '';
+      display: block;
+      background: url(https://static.caimei365.com/www/authentic/h5/icon-search.png)
+        no-repeat;
+      width: 6.4vw;
+      height: 6.4vw;
+      position: absolute;
+      background-size: 6.4vw;
+      left: 1.6vw;
+      top: 1.6vw;
+    }
+
+    input {
+      display: block;
+      width: 100%;
+      height: 9.6vw;
+      line-height: 9.6vw;
+      outline: none;
+    }
   }
 }
 </style>

+ 57 - 0
components/SimpleSwiper/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="simple-swiper">
+    <swiper ref="mySwiper" :options="swiperOptions">
+      <swiper-slide v-for="(item, index) in imageList" :key="index">
+        <div class="slide"><img class="image" :src="item" /></div>
+      </swiper-slide>
+    </swiper>
+  </div>
+</template>
+
+<script>
+import { Swiper, SwiperSlide, directive } from 'vue-awesome-swiper'
+export default {
+  components: {
+    Swiper,
+    SwiperSlide,
+  },
+  directives: {
+    swiper: directive,
+  },
+  props: {
+    imageList: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      swiperOptions: {
+        autoplay: true,
+      },
+    }
+  },
+  computed: {
+    swiper() {
+      return this.$refs.mySwiper.$swiper
+    },
+  },
+  mounted() {
+    console.log('Current Swiper instance object', this.swiper)
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.simple-swiper {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+
+  .image {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 24 - 6
components/SimpleTabs/index.vue

@@ -1,14 +1,22 @@
 <template>
   <div class="tabbar">
     <div class="tab">
-      <div class="item active">文章</div>
-      <div class="item">图片</div>
-      <div class="item">视频</div>
-      <div class="item">文件</div>
-      <div class="item">资料包</div>
+      <div
+        class="item"
+        :class="{ active: current === item.id }"
+        v-for="item in tabs"
+        :key="item.id"
+        v-text="item.name"
+        @click="$emit('change', item)"
+      ></div>
     </div>
     <div class="tab-search">
-      <input type="text" class="control" placeholder="搜索名称" />
+      <input
+        type="text"
+        class="control"
+        placeholder="搜索名称"
+        @keyup.enter="($event) => $emit('search', $event.target.value)"
+      />
     </div>
   </div>
 </template>
@@ -16,6 +24,16 @@
 <script>
 export default {
   name: 'SimpleTabs',
+  props: {
+    tabs: {
+      type: Array,
+      default: () => [],
+    },
+    current: {
+      type: Number,
+      default: 0,
+    },
+  },
 }
 </script>
 

+ 27 - 0
configs/tabs.js

@@ -0,0 +1,27 @@
+export const tabs = () => [
+  {
+    id: 0,
+    path: '/database/article',
+    name: '文章',
+  },
+  {
+    id: 1,
+    path: '/database/image',
+    name: '图片',
+  },
+  {
+    id: 2,
+    path: '/database/video',
+    name: '视频',
+  },
+  {
+    id: 3,
+    path: '/database/file',
+    name: '文件',
+  },
+  {
+    id: 4,
+    path: '/database/package',
+    name: '资料包',
+  },
+]

+ 10 - 0
keys.config.js

@@ -0,0 +1,10 @@
+export default {
+  ph: {
+    appId: 'wx6512b1dfb84c28e1',
+    authUserId: '',
+  },
+  ldm: {
+    appId: 'wx6512b1dfb84c28e1',
+    authUserId: '',
+  },
+}

+ 76 - 2
layouts/app-ldm.vue

@@ -1,15 +1,89 @@
 <template>
-  <div class="layout">
+  <div class="layout" v-if="isMounted">
     <div class="header"></div>
     <div class="content">
       <nuxt />
     </div>
     <div class="footer"></div>
+    <ldm-login></ldm-login>
   </div>
 </template>
 
 <script>
-export default {}
+import { getCookies } from '@/utils/auth'
+import keys from '@/keys.config'
+import { mapGetters } from 'vuex'
+export default {
+  computed: {
+    ...mapGetters(['userInfo', 'type', 'accessToken']),
+  },
+  data() {
+    return {
+      isMounted: false,
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', () => {})
+  },
+  methods: {
+    init() {
+      this.responseWidth()
+      this.initPageData()
+    },
+    // 初始化数据页面公共数据
+    initPageData() {
+      const key = this.$route.path.split('/')[1]
+      // 保存页面入口
+      this.$store.commit('user/SET_TYPE', key)
+      // 保存用户AppId
+      this.$store.commit('user/SET_APPID', keys[key].appId)
+
+      // 获取用户信息
+      const userInfo = getCookies('userInfo')
+      if (userInfo) {
+        this.$store.commit('user/SET_USERINFO', JSON.parse(userInfo))
+      }
+      // 初始化供应商信息
+      this.fetchSupplierInfo()
+    },
+
+    // 获取供应商信息
+    async fetchSupplierInfo() {
+      const appId = this.$store.getters.appId
+      try {
+        const res = await this.$http.api.fetchSupplierInfo({ appId })
+        this.$store.commit('supplier/SET_SUPPLIER_INFO', res.data)
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isMounted = true
+      }
+    },
+
+    // 退出登录
+    logout() {
+      this.$store.dispatch('user/logout')
+    },
+
+    // 回到首页
+    backHome() {
+      const url = '/' + this.type
+      if (this.$route.path === url) return
+      this.$router.replace(url)
+    },
+
+    // 响应页面宽度变化
+    responseWidth() {
+      this.$store.commit('app/SET_SCREEN', window.innerWidth)
+      window.addEventListener('resize', (e) => {
+        this.$store.commit('app/SET_SCREEN', e.target.innerWidth)
+      })
+    },
+  },
+}
 </script>
 
 <style scoped lang="scss"></style>

+ 67 - 6
layouts/app.vue

@@ -1,21 +1,22 @@
 <template>
-  <div class="layout">
+  <div class="layout" v-if="isMounted">
     <div class="header">
       <div class="navbar flex justify-between items-center">
-        <div class="logo flex items-center">
+        <div class="logo flex items-center" @click="backHome">
           <img
             src="https://static.caimei365.com/www/authentic/h5/icon-logo.png"
           />
           <span>认证通</span>
         </div>
         <div class="user-info">
-          <template v-if="false">
-            <span>15872950940</span>
-            <span class="underline logout">退出登录</span>
+          <template v-if="accessToken">
+            <span v-text="userInfo.mobile"></span>
+            <span class="underline logout" @click="logout">退出登录</span>
           </template>
           <template v-else>
             <div
               class="login pr-3 pl-3 border rounded-sm border-white leading-6"
+              @click="$store.commit('app/SHOW_LOGIN')"
             >
               登录
             </div>
@@ -35,15 +36,71 @@
 </template>
 
 <script>
+import { getCookies } from '@/utils/auth'
+import keys from '@/keys.config'
+import { mapGetters } from 'vuex'
 export default {
+  computed: {
+    ...mapGetters(['userInfo', 'type', 'accessToken']),
+  },
+  data() {
+    return {
+      isMounted: false,
+    }
+  },
   mounted() {
     this.init()
   },
-  beforeDestroy() {},
+  beforeDestroy() {
+    window.removeEventListener('resize', () => {})
+  },
   methods: {
     init() {
       this.responseWidth()
+      this.initPageData()
     },
+    // 初始化数据页面公共数据
+    initPageData() {
+      const key = this.$route.path.split('/')[1]
+      // 保存页面入口
+      this.$store.commit('user/SET_TYPE', key)
+      // 保存用户AppId
+      this.$store.commit('user/SET_APPID', keys[key].appId)
+
+      // 获取用户信息
+      const userInfo = getCookies('userInfo')
+      if (userInfo) {
+        this.$store.commit('user/SET_USERINFO', JSON.parse(userInfo))
+      }
+      // 初始化供应商信息
+      this.fetchSupplierInfo()
+    },
+
+    // 获取供应商信息
+    async fetchSupplierInfo() {
+      const appId = this.$store.getters.appId
+      try {
+        const res = await this.$http.api.fetchSupplierInfo({ appId })
+        this.$store.commit('supplier/SET_SUPPLIER_INFO', res.data)
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isMounted = true
+      }
+    },
+
+    // 退出登录
+    logout() {
+      this.$store.dispatch('user/logout')
+    },
+
+    // 回到首页
+    backHome() {
+      const url = '/' + this.type
+      if (this.$route.path === url) return
+      this.$router.replace(url)
+    },
+
     // 响应页面宽度变化
     responseWidth() {
       this.$store.commit('app/SET_SCREEN', window.innerWidth)
@@ -60,10 +117,12 @@ export default {
 @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;
@@ -76,6 +135,7 @@ export default {
       }
 
       .logo {
+        cursor: pointer;
         img {
           display: block;
           width: 44px;
@@ -120,6 +180,7 @@ export default {
       position: fixed;
       top: 0;
       left: 0;
+      z-index: 999;
       width: 100%;
       padding: 0 2.4vw;
       height: 12.8vw;

+ 70 - 0
package-lock.json

@@ -2396,6 +2396,11 @@
         "@popperjs/core": "^2.9.2"
       }
     },
+    "@vant/touch-emulator": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/@vant/touch-emulator/-/touch-emulator-1.3.2.tgz",
+      "integrity": "sha512-Om6e8kCAnmk/q8byngKreff7Hyn6XxwOGr8yedP3y3LEVoE+iyj8/+Mn+AYvGEQ00GK0MlgAfyaV4emXAYj1Hw=="
+    },
     "@vue/babel-helper-vue-jsx-merge-props": {
       "version": "1.2.1",
       "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
@@ -3785,6 +3790,16 @@
       "resolved": "https://registry.npmmirror.com/cli-width/-/cli-width-3.0.0.tgz",
       "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="
     },
+    "clipboard": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.10.tgz",
+      "integrity": "sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==",
+      "requires": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
     "cliui": {
       "version": "7.0.4",
       "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz",
@@ -4776,6 +4791,11 @@
       "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
       "dev": true
     },
+    "delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
+    },
     "delegates": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz",
@@ -4888,6 +4908,14 @@
         }
       }
     },
+    "dom7": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmmirror.com/dom7/-/dom7-2.1.5.tgz",
+      "integrity": "sha512-xnhwVgyOh3eD++/XGtH+5qBwYTgCm0aW91GFgPJ3XG+jlsRLyJivnbP0QmUBFhI+Oaz9FV0s7cxgXHezwOEBYA==",
+      "requires": {
+        "ssr-window": "^2.0.0"
+      }
+    },
     "domain-browser": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -5872,6 +5900,14 @@
         }
       }
     },
+    "good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "requires": {
+        "delegate": "^3.1.2"
+      }
+    },
     "graceful-fs": {
       "version": "4.2.10",
       "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz",
@@ -6992,6 +7028,11 @@
       "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
       "dev": true
     },
+    "js-cookie": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.1.tgz",
+      "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw=="
+    },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -11868,6 +11909,11 @@
       "resolved": "https://registry.npmmirror.com/scule/-/scule-0.2.1.tgz",
       "integrity": "sha512-M9gnWtn3J0W+UhJOHmBxBTwv8mZCan5i1Himp60t6vvZcor0wr+IM0URKmIglsWJ7bRujNAVVN77fp+uZaWoKg=="
     },
+    "select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
+    },
     "semver": {
       "version": "6.3.0",
       "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.0.tgz",
@@ -12315,6 +12361,11 @@
         "tweetnacl": "~0.14.0"
       }
     },
+    "ssr-window": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-2.0.0.tgz",
+      "integrity": "sha512-NXzN+/HPObKAx191H3zKlYomE5WrVIkoCB5IaSdvKokxTpjBdWfr0RaP+1Z5KOfDT0ZVz+2tdtiBkhsEQ9p+0A=="
+    },
     "ssri": {
       "version": "8.0.1",
       "resolved": "https://registry.npmmirror.com/ssri/-/ssri-8.0.1.tgz",
@@ -12567,6 +12618,15 @@
         "util.promisify": "~1.0.0"
       }
     },
+    "swiper": {
+      "version": "5.4.5",
+      "resolved": "https://registry.npmmirror.com/swiper/-/swiper-5.4.5.tgz",
+      "integrity": "sha512-7QjA0XpdOmiMoClfaZ2lYN6ICHcMm72LXiY+NF4fQLFidigameaofvpjEEiTQuw3xm5eksG5hzkaRsjQX57vtA==",
+      "requires": {
+        "dom7": "^2.1.5",
+        "ssr-window": "^2.0.0"
+      }
+    },
     "tailwind-config-viewer": {
       "version": "1.6.3",
       "resolved": "https://registry.npmmirror.com/tailwind-config-viewer/-/tailwind-config-viewer-1.6.3.tgz",
@@ -12959,6 +13019,11 @@
       "resolved": "https://registry.npmmirror.com/timsort/-/timsort-0.3.0.tgz",
       "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A=="
     },
+    "tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    },
     "tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.0.33.tgz",
@@ -13427,6 +13492,11 @@
       "resolved": "https://registry.npmmirror.com/vue/-/vue-2.6.14.tgz",
       "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
     },
+    "vue-awesome-swiper": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/vue-awesome-swiper/-/vue-awesome-swiper-4.1.1.tgz",
+      "integrity": "sha512-50um10t6N+lJaORkpwSi1wWuMmBI1sgFc9Znsi5oUykw2cO5DzLaBHcO2JNX21R+Ue4TGoIJDhhxjBHtkFrTEQ=="
+    },
     "vue-client-only": {
       "version": "2.1.0",
       "resolved": "https://registry.npmmirror.com/vue-client-only/-/vue-client-only-2.1.0.tgz",

+ 5 - 0
package.json

@@ -14,10 +14,15 @@
   },
   "dependencies": {
     "@nuxtjs/axios": "^5.13.6",
+    "@vant/touch-emulator": "^1.3.2",
+    "clipboard": "^2.0.10",
     "core-js": "^3.19.3",
+    "js-cookie": "^3.0.1",
     "nuxt": "^2.15.8",
+    "swiper": "^5.4.5",
     "vant": "^2.12.47",
     "vue": "^2.6.14",
+    "vue-awesome-swiper": "^4.1.1",
     "vue-server-renderer": "^2.6.14",
     "vue-template-compiler": "^2.6.14",
     "webpack": "^4.46.0"

+ 75 - 24
pages/ldm/approve/club/detail.vue

@@ -1,38 +1,36 @@
 <template>
   <div class="page">
     <div class="page-top">
-      <div class="swiper"></div>
+      <div class="swiper">
+        <SimpleSwiper :imageList="clubInfo.bannerList"></SimpleSwiper>
+      </div>
       <div class="club-info">
-        <img class="logo" src="https://picsum.photos/200/200" />
-        <div class="name">丽颜科美复兴广场店</div>
-        <div class="remark">认证设备:LDM®-noblesse</div>
+        <img class="logo" :src="clubInfo.logo" />
+        <div class="name" v-text="clubInfo.authParty"></div>
+        <div class="remark" v-if="clubInfo.remark">
+          认证设备:{{ clubInfo.remark }}
+        </div>
       </div>
     </div>
     <div class="page-content">
       <div class="club-info">
-        <div class="address">
-          深圳市宝钢区一时路街188号好吃不上火美丽广场1105店
-        </div>
-        <div class="mobile">0755-25777189</div>
+        <div class="address" v-text="address"></div>
+        <div class="mobile">{{ clubInfo.mobile | formatEmpty }}</div>
       </div>
       <!-- 列表标题 -->
       <div class="title">明星操作师</div>
       <!-- 列表 -->
       <div class="list">
-        <div class="section flex items-center">
-          <img class="cover" src="https://picsum.photos/200/200" />
-          <div class="info">
-            <div class="name">宋医师</div>
-            <div class="tag">皮肤美容科 | 主任医师│从业24年</div>
-            <div class="more" @click="toDetail">点击查看</div>
-          </div>
-        </div>
-        <div class="section flex items-center">
-          <img class="cover" src="https://picsum.photos/200/200" />
+        <div
+          class="section flex items-center"
+          v-for="(item, index) in clubInfo.doctorList"
+          :key="index"
+        >
+          <img :src="item.doctorImage" class="cover" />
           <div class="info">
-            <div class="name">宋医师</div>
-            <div class="tag">皮肤美容科 | 主任医师│从业24年</div>
-            <div class="more">点击查看</div>
+            <div class="name" v-text="item.doctorName"></div>
+            <div class="tag" v-text="item.tagList.join(' | ')"></div>
+            <div class="more" @click="toDetail(item)">点击查看</div>
           </div>
         </div>
       </div>
@@ -41,12 +39,68 @@
 </template>
 
 <script>
+import { drawLogo } from '@/utils'
+import { mapNavigate } from '@/utils/map-utils'
 export default {
   layout: 'app-ldm',
+  filters: {
+    formatEmpty(val) {
+      return val || '未知'
+    },
+    formatSnCode(code) {
+      if (!code) return ''
+      return code.replace(/^(\w{2})\w+(\w{4})$/, '$1******$2')
+    },
+  },
+  data() {
+    return {
+      clubInfo: {},
+    }
+  },
+  computed: {
+    address() {
+      return this.clubInfo.area + this.clubInfo.address || '未知'
+    },
+    isEmpty() {
+      return this.clubInfo.productList
+        ? this.clubInfo.productList.length === 0
+        : true
+    },
+  },
+  mounted() {
+    this.initData()
+  },
   methods: {
     toDetail() {
+      localStorage.setItem('doctorInfo', JSON.stringify(row))
       this.$router.push('/ldm/approve/personnel/operate/detail')
     },
+    // 初始化
+    initData() {
+      const clubInfo = localStorage.getItem('clubInfo')
+      if (clubInfo) {
+        this.clubInfo = JSON.parse(clubInfo)
+        this.fetchDetail()
+      }
+    },
+    // 获取机构详细信息
+    async fetchDetail() {
+      try {
+        const authId = this.clubInfo.authId
+        const res = await this.$http.api.getAuthClubDetail({ authId })
+        this.clubInfo = { ...this.clubInfo, ...res.data } // 合并
+      } catch (error) {
+        console.log(error)
+      }
+      if (this.clubInfo.bannerList.length <= 0) {
+        this.clubInfo.bannerList.push('/placeholder.png')
+      }
+      if (!this.clubInfo.logo) {
+        this.clubInfo.logo = drawLogo(this.clubInfo.authParty)
+      }
+    },
+    // 导航
+    navigation() {},
   },
 }
 </script>
@@ -66,7 +120,6 @@ export default {
     .swiper {
       width: 452px;
       height: 452px;
-      background: pink;
     }
 
     .club-info {
@@ -215,9 +268,7 @@ export default {
     height: 137vw;
 
     .swiper {
-      width: 100vw;
       height: 100vw;
-      background: pink;
     }
 
     .club-info {

+ 179 - 19
pages/ldm/approve/club/index.vue

@@ -4,42 +4,196 @@
     <div class="page-content">
       <!-- 搜索区域 -->
       <div class="search flex justify-center">
-        <input type="text" placeholder="搜索店铺" />
+        <input
+          type="text"
+          placeholder="搜索店铺"
+          v-model="listQuery.clubName"
+          @keyup.enter="onSearch"
+        />
       </div>
       <!-- 地区筛选 -->
-      <div class="city"></div>
+      <div class="city">
+        <LdmCity
+          @change="onCityChange"
+          :options="cityList"
+          labelName="name"
+          valueName="id"
+        ></LdmCity>
+      </div>
       <!-- 标题 -->
       <div class="title">距你最近...</div>
       <!-- 机构列表 -->
       <div class="list">
-        <div class="section flex items-center">
-          <img src="https://picsum.photos/200/200" class="cover" />
-          <div class="info">
-            <div class="name">美丽肌肤体验店</div>
-            <div class="line"></div>
-            <div class="mobile">0755-25777189</div>
-            <div class="address">深圳市宝钢区一时路街188号好吃不上火...</div>
-            <div class="distance">9999km</div>
-          </div>
-        </div>
-        <div class="section flex items-center">
-          <img src="https://picsum.photos/200/200" class="cover" />
+        <div
+          class="section flex items-center"
+          v-for="item in list"
+          :key="item.authId"
+          @click="toDetail(item)"
+        >
+          <img class="cover" :src="item.logo || drawLogo(item.clubName)" />
           <div class="info">
-            <div class="name">美丽肌肤体验店</div>
+            <div class="name" v-text="item.clubName"></div>
             <div class="line"></div>
-            <div class="mobile">0755-25777189</div>
-            <div class="address">深圳市宝钢区一时路街188号好吃不上火...</div>
-            <div class="distance">9999km</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>
       </div>
+
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { loactionSelf } from '@/utils/map-utils'
+import { drawLogo } from '@/utils'
 export default {
   layout: 'app-ldm',
+  data() {
+    return {
+      isRequest: true,
+      list: [],
+      listQuery: {
+        appId: '',
+        lngAndLat: '',
+        clubName: '',
+        provinceId: '',
+        cityId: '',
+        townId: '',
+        pageNum: 1,
+        pageSize: 4,
+      },
+      total: 0,
+      cityList: [],
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'appId']),
+    emptyList() {
+      return 3 - (this.list.length % 3)
+    },
+  },
+  mounted() {
+    this.initData()
+    this.fetchCityList()
+  },
+  methods: {
+    // 绘制logo的方法
+    drawLogo,
+    // 查看详情
+    toDetail(item) {
+      localStorage.setItem('clubInfo', JSON.stringify(item))
+      this.$router.push('/ldm/approve/club/detail')
+    },
+    // 初始化页面数据
+    async initData() {
+      // 自定义加载图标
+      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.listQuery.appId = this.appId
+      // 获取机构列表
+      this.fetchList()
+    },
+    // 获取机构列表
+    async fetchList() {
+      try {
+        const res = await this.$http.api.getAuthClubList(this.listQuery)
+        this.total = res.data.total
+        this.list = res.data.list
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.$toast.clear()
+        this.isRequest = false
+      }
+    },
+    // 获取地址列表
+    fetchCityList() {
+      this.$http.api.fetchAllCityList().then((res) => {
+        this.cityList = res.data
+      })
+    },
+    // 城市变化
+    onCityChange(selectValue) {
+      this.listQuery.provinceId = ''
+      this.listQuery.cityId = ''
+      this.listQuery.townId = ''
+
+      selectValue.map((item, index) => {
+        if (index === 0) {
+          this.listQuery.provinceId = item.id
+        } else if (index === 1) {
+          this.listQuery.cityId = item.id
+        } else {
+          this.listQuery.townId = item.id
+        }
+      })
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 搜索
+    onSearch() {
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 格式化地址
+    formatAddress(a1, a2) {
+      let resutl = ''
+      if (typeof a1 === 'string') {
+        resutl += a1
+      }
+      if (typeof a2 === 'string') {
+        resutl += a2
+      }
+      return resutl || '未知'
+    },
+  },
+  beforeDestroy() {
+    this.$toast.clear()
+  },
 }
 </script>
 
@@ -53,6 +207,7 @@ export default {
   }
   .page-content {
     width: 836px;
+    padding-bottom: 80px;
     margin: 0 auto;
     overflow: hidden;
     .search {
@@ -102,6 +257,7 @@ export default {
         padding: 16px;
         box-sizing: border-box;
         margin-bottom: 20px;
+        cursor: pointer;
 
         .cover {
           width: 92px;
@@ -250,7 +406,11 @@ export default {
         border-radius: 2vw;
         padding: 3.2vw;
         box-sizing: border-box;
-        margin: 5.7vw 0;
+        margin-top: 5.7vw;
+
+        &:last-child {
+          margin-bottom: 5.7vw;
+        }
 
         .cover {
           width: 21.5vw;

+ 6 - 7
pages/ldm/approve/index.vue

@@ -20,8 +20,12 @@
         </div>
       </div>
       <div class="entry">
-        <div class="section">查询授权商家</div>
-        <div class="section">查询官方培训师</div>
+        <nuxt-link class="section" to="/ldm/approve/club"
+          >查询授权商家</nuxt-link
+        >
+        <nuxt-link class="section" to="/ldm/approve/personnel/training"
+          >查询官方培训师</nuxt-link
+        >
       </div>
     </div>
     <div class="page-footer flex flex-col justify-center">
@@ -34,11 +38,6 @@
 <script>
 export default {
   layout: 'app-ldm',
-  methods: {
-    toDetail() {
-      this.$router.push('/ldm/approve/personnel/operate/detail')
-    },
-  },
 }
 </script>
 

+ 47 - 14
pages/ldm/approve/personnel/operate/detail.vue

@@ -1,24 +1,24 @@
 <template>
   <div class="page">
     <div class="page-title">专业美容培训师</div>
-    <div class="page-top"><div class="swiper"></div></div>
+    <div class="page-top">
+      <div class="swiper">
+        <SimpleSwiper :imageList="doctorInfo.bannerList"></SimpleSwiper>
+      </div>
+    </div>
     <div class="page-content">
       <div class="doctor-info">
-        <div class="name">宋医师</div>
-        <div class="tag">皮肤美容科 | 主任医师 | 从业24年</div>
+        <div class="name">{{ doctorInfo.doctorName }}</div>
+        <div class="tag">{{ doctorInfo.tagList.join(' | ') }}</div>
       </div>
       <div class="param-list">
-        <div class="param">
-          <div class="name">个人简介:</div>
-          <div class="content">
-            宋美丽,医学博士,主任医师。从事皮肤性病医教研工作三十多年。
-            在皮肤素病、皮肤血管病、结缔组织病,化妆品皮肤损伤、皮肤激光
-            医学、医疗美容技术等均有较深的造诣
-          </div>
-        </div>
-        <div class="param">
-          <div class="name">擅长:</div>
-          <div class="content">面部皮肤管理,女性健康保养</div>
+        <div
+          class="param"
+          v-for="(param, index) in doctorInfo.paramList"
+          :key="index"
+        >
+          <div class="name" v-text="param.name"></div>
+          <div class="content" v-text="param.content"></div>
         </div>
       </div>
     </div>
@@ -28,6 +28,39 @@
 <script>
 export default {
   layout: 'app-ldm',
+  data() {
+    return {
+      doctorInfo: {
+        tagList: [],
+        paramList: [],
+      },
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      const doctorInfo = localStorage.getItem('doctorInfo')
+      if (doctorInfo) {
+        this.doctorInfo = JSON.parse(doctorInfo)
+        this.fetchDetail()
+      }
+    },
+    async fetchDetail() {
+      try {
+        const res = await this.$http.api.fetchDoctorDetail({
+          doctorId: this.doctorInfo.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>
 

+ 47 - 14
pages/ldm/approve/personnel/training/detail.vue

@@ -1,24 +1,24 @@
 <template>
   <div class="page">
     <div class="page-title">专业美容培训师</div>
-    <div class="page-top"><div class="swiper"></div></div>
+    <div class="page-top">
+      <div class="swiper">
+        <SimpleSwiper :imageList="doctorInfo.bannerList"></SimpleSwiper>
+      </div>
+    </div>
     <div class="page-content">
       <div class="doctor-info">
-        <div class="name">宋医师<i /></div>
-        <div class="tag">皮肤美容科 | 主任医师 | 从业24年</div>
+        <div class="name">{{ doctorInfo.doctorName }}<i /></div>
+        <div class="tag">{{ doctorInfo.tagList.join(' | ') }}</div>
       </div>
       <div class="param-list">
-        <div class="param">
-          <div class="name">个人简介:</div>
-          <div class="content">
-            宋美丽,医学博士,主任医师。从事皮肤性病医教研工作三十多年。
-            在皮肤素病、皮肤血管病、结缔组织病,化妆品皮肤损伤、皮肤激光
-            医学、医疗美容技术等均有较深的造诣
-          </div>
-        </div>
-        <div class="param">
-          <div class="name">擅长:</div>
-          <div class="content">面部皮肤管理,女性健康保养</div>
+        <div
+          class="param"
+          v-for="(param, index) in doctorInfo.paramList"
+          :key="index"
+        >
+          <div class="name" v-text="param.name"></div>
+          <div class="content" v-text="param.content"></div>
         </div>
       </div>
     </div>
@@ -28,6 +28,39 @@
 <script>
 export default {
   layout: 'app-ldm',
+  data() {
+    return {
+      doctorInfo: {
+        tagList: [],
+        paramList: [],
+      },
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      const doctorInfo = localStorage.getItem('doctorInfo')
+      if (doctorInfo) {
+        this.doctorInfo = JSON.parse(doctorInfo)
+        this.fetchDetail()
+      }
+    },
+    async fetchDetail() {
+      try {
+        const res = await this.$http.api.fetchDoctorDetail({
+          doctorId: this.doctorInfo.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>
 

+ 71 - 21
pages/ldm/approve/personnel/training/index.vue

@@ -4,38 +4,88 @@
     <div class="page-content">
       <div class="title">专业美容培训师</div>
       <div class="list">
-        <div class="section flex items-center">
-          <img class="cover" src="https://picsum.photos/200/200" />
+        <div
+          class="section flex items-center"
+          v-for="item in list"
+          :key="item.doctorId"
+        >
+          <img class="cover" :src="item.doctorImage" />
           <div class="info">
-            <div class="name">宋医师<i /></div>
-            <div class="tag">皮肤美容科 | 主任医师│从业24年</div>
-            <div class="more">点击查看</div>
-          </div>
-        </div>
-        <div class="section flex items-center">
-          <img class="cover" src="https://picsum.photos/200/200" />
-          <div class="info">
-            <div class="name">宋医师<i /></div>
-            <div class="tag">皮肤美容科 | 主任医师│从业24年</div>
-            <div class="more">点击查看</div>
-          </div>
-        </div>
-        <div class="section flex items-center">
-          <img class="cover" src="https://picsum.photos/200/200" />
-          <div class="info">
-            <div class="name">宋医师<i /></div>
-            <div class="tag">皮肤美容科 | 主任医师│从业24年</div>
-            <div class="more">点击查看</div>
+            <div class="name">{{ item.doctorName }}<i /></div>
+            <div class="tag">{{ item.tagList.join(' | ') }}</div>
+            <div class="more" @click="toDetail(item)">点击查看</div>
           </div>
         </div>
       </div>
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
 export default {
   layout: 'app-ldm',
+  data() {
+    return {
+      isRequest: false,
+      listQuery: {
+        appId: '',
+        doctorType: 2,
+        doctorName: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'appId']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    async fetchList() {
+      try {
+        this.listQuery.appId = this.appId
+        const res = await this.$http.api.fetchDoctorList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // 搜索
+    onSearch() {
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 医师详情
+    toDetail(item) {
+      localStorage.setItem('doctorInfo', JSON.stringify(item))
+      this.$router.push('/ldm/approve/personnel/training/detail')
+    },
+  },
 }
 </script>
 

+ 68 - 29
pages/ldm/database/package.vue

@@ -12,46 +12,85 @@
     </div>
     <div class="page-content">
       <div class="list">
-        <div class="section flex justify-between items-center">
+        <div
+          class="section flex justify-between items-center"
+          v-for="item in list"
+          :key="item.fileId"
+        >
           <div class="info">
-            <div class="name">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="download">点击下载</div>
-          </div>
-        </div>
-        <div class="section flex justify-between items-center">
-          <div class="info">
-            <div class="name">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="download">点击下载</div>
-          </div>
-        </div>
-        <div class="section flex justify-between items-center">
-          <div class="info">
-            <div class="name">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="download">点击下载</div>
-          </div>
-        </div>
-        <div class="section flex justify-between items-center">
-          <div class="info">
-            <div class="name">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="download">点击下载</div>
+            <div class="name" v-text="item.fileName"></div>
+            <div class="download" @click="downloadLink(item)">点击下载</div>
           </div>
         </div>
       </div>
+
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import downloadLink from '@/utils/download-link'
+
 export default {
   layout: 'app-ldm',
+  data() {
+    return {
+      isRequest: true,
+      listQuery: {
+        fileType: 2,
+        fileTitle: '',
+        authUserId: '',
+        pageNum: 1,
+        pageSize: 4,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'supplierInfo']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 下载方法
+    downloadLink(item) {
+      const url = `${process.env.BASE_URL}/download/file?ossName=${item.fileDownloadUrl}&fileName=${item.fileName}`
+      downloadLink(url)
+    },
+    // 获取列表
+    async fetchList() {
+      try {
+        this.listQuery.authUserId = this.userInfo.authUserId
+        const res = await this.$http.api.getFileList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+  },
 }
 </script>
 

+ 7 - 1
pages/ldm/index.vue

@@ -23,7 +23,6 @@
       <div class="name mb-1">需要帮助吗?</div>
       <div class="contact">联系我们 +86 1382160616</div>
     </div>
-    <ldm-login></ldm-login>
   </div>
 </template>
 
@@ -53,8 +52,15 @@ export default {
       ],
     }
   },
+
   methods: {
     toDetail(item) {
+      const hasLogin = this.$store.getters.accessToken
+      if (item.id > 0 && !hasLogin) {
+        this.$toast({ message: '请先登录', duration: 1000 })
+        this.$store.commit('app/SHOW_LOGIN')
+        return
+      }
       this.$router.push(item.path)
     },
   },

+ 85 - 39
pages/ph/approve/club/detail.vue

@@ -2,71 +2,117 @@
   <div class="page md:flex md:justify-between">
     <div class="page-title">机构认证</div>
     <div class="page-top">
-      <div class="swiper bg-pink-100"></div>
+      <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">丽颜科美复兴广场店</div>
-            <div class="mobile">0755-25777189</div>
-            <div class="address">上海市黄浦区马当路388号复兴广场502号</div>
+            <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="https://picsum.photos/400/400" /></div>
+          <div class="logo"><img :src="clubInfo.logo" /></div>
         </div>
         <div class="section flex justify-between items-center mt-6">
           <div class="navigation">导航</div>
-          <div class="distance">9999km</div>
+          <div class="distance" v-text="'距你' + clubInfo.distance + 'km'">
+            9999km
+          </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">
-            <div class="info">
-              <div class="name">日本水素水细胞仪</div>
-              <div class="code">SN码:SR******8088</div>
-            </div>
-            <div class="detail">查看认证</div>
-          </div>
-          <div class="device flex justify-between items-center">
+          <div
+            class="device flex justify-between items-center"
+            v-for="item in clubInfo.productList"
+            :key="item.productId"
+          >
             <div class="info">
-              <div class="name">日本水素水细胞仪</div>
-              <div class="code">SN码:SR******8088</div>
+              <div class="name" v-text="item.productName"></div>
+              <div class="code">SN码:{{ item.snCode | formatSnCode }}</div>
             </div>
-            <div class="detail">查看认证</div>
-          </div>
-          <div class="device flex justify-between items-center">
-            <div class="info">
-              <div class="name">日本水素水细胞仪</div>
-              <div class="code">SN码:SR******8088</div>
-            </div>
-            <div class="detail">查看认证</div>
-          </div>
-          <div class="device flex justify-between items-center">
-            <div class="info">
-              <div class="name">日本水素水细胞仪</div>
-              <div class="code">SN码:SR******8088</div>
-            </div>
-            <div class="detail">查看认证</div>
-          </div>
-          <div class="device flex justify-between items-center">
-            <div class="info">
-              <div class="name">日本水素水细胞仪</div>
-              <div class="code">SN码:SR******8088</div>
-            </div>
-            <div class="detail">查看认证</div>
+            <div class="detail" @click="toDetail(item)">查看认证</div>
           </div>
         </div>
       </div>
+      <SimpleEmpty
+        v-if="isEmpty"
+        name="icon-empty-device.png"
+        description="暂无已认证设备"
+      ></SimpleEmpty>
     </div>
   </div>
 </template>
 
 <script>
+import { drawLogo } from '@/utils'
+import { mapNavigate } from '@/utils/map-utils'
 export default {
   layout: 'app',
+  filters: {
+    formatEmpty(val) {
+      return val || '未知'
+    },
+    formatSnCode(code) {
+      if (!code) return ''
+      return code.replace(/^(\w{2})\w+(\w{4})$/, '$1******$2')
+    },
+  },
+  data() {
+    return {
+      clubInfo: {},
+    }
+  },
+  computed: {
+    address() {
+      return this.clubInfo.area + this.clubInfo.address || '未知'
+    },
+    isEmpty() {
+      return this.clubInfo.productList
+        ? this.clubInfo.productList.length === 0
+        : true
+    },
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    // 设备详情
+    toDetail(item) {
+      window.location.href = `${process.env.CIMEI_LOCAL}/product/auth/product-${item.productId}.html`
+    },
+    // 初始化
+    initData() {
+      const clubInfo = localStorage.getItem('clubInfo')
+      if (clubInfo) {
+        this.clubInfo = JSON.parse(clubInfo)
+        this.fetchDetail()
+      }
+    },
+    // 获取机构详细信息
+    async fetchDetail() {
+      try {
+        const authId = this.clubInfo.authId
+        const res = await this.$http.api.getAuthClubDetail({ authId })
+        this.clubInfo = { ...this.clubInfo, ...res.data } // 合并
+      } catch (error) {
+        console.log(error)
+      }
+      if (this.clubInfo.bannerList.length <= 0) {
+        this.clubInfo.bannerList.push('/placeholder.png')
+      }
+      if (!this.clubInfo.logo) {
+        this.clubInfo.logo = drawLogo(this.clubInfo.authParty)
+      }
+    },
+    // 导航
+    navigation() {},
+  },
 }
 </script>
 
@@ -184,6 +230,7 @@ export default {
         background-color: #ffe6e8;
         font-size: 16px;
         color: #bc1724;
+        cursor: pointer;
 
         &::after {
           content: '';
@@ -252,7 +299,6 @@ export default {
 
   .page-top {
     .swiper {
-      width: 100vw;
       height: 100vw;
     }
   }

+ 198 - 41
pages/ph/approve/club/index.vue

@@ -1,64 +1,203 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <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="search">
-        <simple-search />
+        <simple-search v-model="listQuery.clubName" @search="onSearch" />
       </div>
       <!-- 地区选择 -->
-      <div class="city bg-red-300">地区选择</div>
+      <div class="city">
+        <SimpleCity
+          @change="onCityChange"
+          :options="cityList"
+          labelName="name"
+          valueName="id"
+        ></SimpleCity>
+      </div>
       <!-- 标题 -->
       <div class="title flex justify-between px-4 pt-8 pb-8 md:px-0">
-        <div>距你最近...</div>
-        <div>共<span>30</span>家机构</div>
+        <div>距最近...</div>
+        <div>共<span v-text="total"></span>家机构</div>
       </div>
       <!-- 列表 -->
       <div class="list">
-        <div class="section flex justify-between mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="info">
-            <div class="name">丽颜科美复兴广场店</div>
-            <div class="mobile">0755-25777189</div>
-            <div class="address">
-              上海市黄浦区马当路388号复兴广场夜丽秀夜丽秀
-            </div>
-            <div class="distance">9999km</div>
-          </div>
-        </div>
-        <div class="section flex justify-between mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="info">
-            <div class="name">丽颜科美复兴广场店</div>
-            <div class="mobile">0755-25777189</div>
-            <div class="address">
-              上海市黄浦区马当路388号复兴广场夜丽秀夜丽秀
+        <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 class="distance">9999km</div>
           </div>
-        </div>
-        <div class="section flex justify-between mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="info">
-            <div class="name">丽颜科美复兴广场店</div>
-            <div class="mobile">0755-25777189</div>
-            <div class="address">
-              上海市黄浦区马当路388号复兴广场夜丽秀夜丽秀
-            </div>
-            <div class="distance">9999km</div>
-          </div>
-        </div>
+        </template>
+        <div class="empty" v-for="i in emptyList" :key="i"></div>
       </div>
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { loactionSelf } from '@/utils/map-utils'
+import { drawLogo } from '@/utils'
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: true,
+      list: [],
+      listQuery: {
+        appId: '',
+        lngAndLat: '',
+        clubName: '',
+        provinceId: '',
+        cityId: '',
+        townId: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      total: 0,
+      cityList: [],
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'appId']),
+    emptyList() {
+      return 3 - (this.list.length % 3)
+    },
+  },
+  mounted() {
+    this.initData()
+    this.fetchCityList()
+  },
+  methods: {
+    // 绘制logo的方法
+    drawLogo,
+    // 查看详情
+    toDetail(item) {
+      localStorage.setItem('clubInfo', JSON.stringify(item))
+      this.$router.push('/ph/approve/club/detail')
+    },
+    // 初始化页面数据
+    async initData() {
+      // 自定义加载图标
+      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.listQuery.appId = this.appId
+      // 获取机构列表
+      this.fetchList()
+    },
+    // 获取机构列表
+    async fetchList() {
+      try {
+        const res = await this.$http.api.getAuthClubList(this.listQuery)
+        this.total = res.data.total
+        this.list = res.data.list
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.$toast.clear()
+        this.isRequest = false
+      }
+    },
+    // 获取地址列表
+    fetchCityList() {
+      this.$http.api.fetchAllCityList().then((res) => {
+        this.cityList = res.data
+      })
+    },
+    // 城市变化
+    onCityChange(selectValue) {
+      this.listQuery.provinceId = ''
+      this.listQuery.cityId = ''
+      this.listQuery.townId = ''
+
+      selectValue.map((item, index) => {
+        if (index === 0) {
+          this.listQuery.provinceId = item.id
+        } else if (index === 1) {
+          this.listQuery.cityId = item.id
+        } else {
+          this.listQuery.townId = item.id
+        }
+      })
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 搜索
+    onSearch() {
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 格式化地址
+    formatAddress(a1, a2) {
+      let resutl = ''
+      if (typeof a1 === 'string') {
+        resutl += a1
+      }
+      if (typeof a2 === 'string') {
+        resutl += a2
+      }
+      return resutl || '未知'
+    },
+  },
+  beforeDestroy() {
+    this.$toast.clear()
+  },
 }
 </script>
 
@@ -66,6 +205,7 @@ export default {
 // pc 端
 @media screen and (min-width: 768px) {
   .page {
+    position: relative;
     min-height: calc(100vh - 80px - 80px);
     background-color: #fff;
   }
@@ -83,18 +223,28 @@ export default {
       font-size: 30px;
       color: #fff;
     }
+
+    .logo,
+    .name {
+      transform: translateY(-60px);
+    }
   }
   .page-content {
     width: 1200px;
     margin: 0 auto;
-    position: relative;
     .search {
-      display: none;
+      position: absolute;
+      left: 50%;
+      top: 260px;
+      transform: translateX(-50%);
     }
 
     .city {
-      padding-top: 12vw;
-      display: none;
+      position: absolute;
+      left: 50%;
+      top: 320px;
+      transform: translateX(-50%);
+      z-index: 9;
     }
 
     .title {
@@ -110,6 +260,11 @@ export default {
       display: flex;
       align-items: center;
       justify-content: space-between;
+      flex-wrap: wrap;
+
+      .empty {
+        width: 390px;
+      }
 
       .section {
         width: 390px;
@@ -216,6 +371,8 @@ export default {
     }
 
     .city {
+      position: relative;
+      z-index: 9;
       padding-top: 12vw;
     }
 

+ 103 - 19
pages/ph/approve/device/list.vue → pages/ph/approve/device/_id.vue

@@ -7,33 +7,47 @@
     <div class="page-content">
       <!-- 搜索区域 -->
       <div class="search">
-        <simple-search />
+        <SimpleSearch v-model="listQuery.snCode" @search="onSearch" />
       </div>
       <!-- 标题 -->
-      <div class="title px-4 pt-12 pb-6 md:px-0">共<span>4</span>台设备</div>
+      <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">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="info">
-            <div class="name">日本水素水细胞仪</div>
-            <div class="code">SN码:SR******8088</div>
-            <div class="club-name">
-              所属机构:<span>丽颜科美复兴广场店</span>
-            </div>
-          </div>
-        </div>
-        <div class="section flex justify-between mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
+        <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">日本水素水细胞仪</div>
-            <div class="code">SN码:SR******8088</div>
+            <div class="name" v-text="item.productName"></div>
+            <div class="code">SN码:{{ item.snCode }}</div>
             <div class="club-name">
-              所属机构:<span>丽颜科美复兴广场店</span>
+              所属机构:<span @click="toClubDetail(item)">{{
+                item.clubName
+              }}</span>
             </div>
           </div>
         </div>
+
+        <div class="empty" v-for="i in emptyList" :key="i"></div>
       </div>
+
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
@@ -41,6 +55,63 @@
 <script>
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: false,
+      listQuery: {
+        productTypeId: '',
+        snCode: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    isEmpty() {
+      return this.list.length === 0
+    },
+    emptyList() {
+      return 3 - (this.list.length % 3)
+    },
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    async fetchList() {
+      try {
+        this.listQuery.productTypeId = this.$route.params.id
+        const res = await this.$http.api.getAuthProductList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // 搜索
+    onSearch() {
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 设备详情
+    toDetail() {
+      window.location.href = `${process.env.CIMEI_LOCAL}/product/auth/product-${item.productId}.html`
+    },
+    // 机构详情
+    toClubDetail() {
+      localStorage.setItem('clubInfo', JSON.stringify({ authId: item.authId }))
+      this.$router.push('/ph/approve/club/detail')
+    },
+  },
 }
 </script>
 
@@ -48,6 +119,7 @@ export default {
 // pc 端
 @media screen and (min-width: 768px) {
   .page {
+    position: relative;
     min-height: calc(100vh - 80px - 80px);
     background-color: #fff;
   }
@@ -65,13 +137,21 @@ export default {
       font-size: 30px;
       color: #fff;
     }
+
+    .logo,
+    .name {
+      transform: translateY(-30px);
+    }
   }
   .page-content {
     width: 1200px;
     margin: 0 auto;
-    position: relative;
+
     .search {
-      display: none;
+      position: absolute;
+      left: 50%;
+      top: 300px;
+      transform: translateX(-50%);
     }
 
     .title {
@@ -89,6 +169,10 @@ export default {
       flex-wrap: wrap;
       justify-content: space-between;
 
+      .empty {
+        width: 390px;
+      }
+
       .section {
         width: 390px;
         height: 108px;

+ 97 - 26
pages/ph/approve/device/index.vue

@@ -1,46 +1,102 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <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="search">
-        <simple-search />
+        <SimpleSearch v-model="listQuery.productName" @search="onSearch" />
       </div>
       <!-- 标题 -->
-      <div class="title px-4 pt-12 pb-6">共<span>30</span>家机构</div>
+      <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">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="name">日本水素水细胞仪</div>
-        </div>
-        <div class="section flex items-center mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="name">日本水素水细胞仪</div>
-        </div>
-        <div class="section flex items-center mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="name">日本水素水细胞仪</div>
-        </div>
-        <div class="section flex items-center mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="name">日本水素水细胞仪</div>
-        </div>
-        <div class="section flex items-center mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="name">日本水素水细胞仪</div>
-        </div>
+        <nuxt-link
+          class="section flex items-center mb-4"
+          v-for="item in list"
+          :key="item.productTypeId"
+          :to="'/ph/approve/device/' + item.productTypeId"
+        >
+          <img class="cover" :src="item.image" />
+          <div class="name" v-text="item.name"></div>
+        </nuxt-link>
+        <div class="empty" v-for="i in emptyList" :key="i"></div>
       </div>
+
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: true,
+      listQuery: {
+        appId: '',
+        productName: '',
+        pageNum: 1,
+        pageSize: 10,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'appId']),
+    emptyList() {
+      return 3 - (this.list.length % 3)
+    },
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 获取设备分类
+    async fetchList() {
+      try {
+        this.listQuery.appId = this.appId
+        const res = await this.$http.api.getAuthProductCateList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // 搜索
+    onSearch() {
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+  },
 }
 </script>
 
@@ -48,6 +104,7 @@ export default {
 // pc 端
 @media screen and (min-width: 768px) {
   .page {
+    position: relative;
     min-height: calc(100vh - 80px - 80px);
     background-color: #fff;
   }
@@ -65,13 +122,21 @@ export default {
       font-size: 30px;
       color: #fff;
     }
+
+    .logo,
+    .name {
+      transform: translateY(-30px);
+    }
   }
   .page-content {
     width: 1200px;
     margin: 0 auto;
-    position: relative;
+
     .search {
-      display: none;
+      position: absolute;
+      left: 50%;
+      top: 300px;
+      transform: translateX(-50%);
     }
 
     .title {
@@ -88,6 +153,11 @@ export default {
       align-items: center;
       flex-wrap: wrap;
       justify-content: space-between;
+
+      .empty {
+        width: 390px;
+      }
+
       .section {
         width: 390px;
         height: 108px;
@@ -97,6 +167,7 @@ export default {
         padding: 16px;
         cursor: pointer;
         transition: all 0.4s;
+
         &:hover {
           box-shadow: 0 0 24px rgba(0, 0, 0, 0.2);
         }

+ 6 - 2
pages/ph/approve/index.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="name mt-2" v-text="supplierInfo.shopName"></div>
     </div>
     <div class="page-content">
       <div class="list">
@@ -22,6 +22,7 @@
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
 export default {
   layout: 'app',
   data() {
@@ -53,6 +54,9 @@ export default {
       ],
     }
   },
+  computed: {
+    ...mapGetters(['supplierInfo']),
+  },
   methods: {
     toDetail(item) {
       this.$router.push(item.path)

+ 59 - 28
pages/ph/approve/personnel/operate/detail.vue

@@ -1,45 +1,43 @@
 <template>
   <div class="page md:flex md:justify-between">
     <div class="page-title">医师认证</div>
-    <div class="page-top"><div class="swiper bg-pink-100"></div></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">宋医师</div>
-        <div class="tag pb-1">主任医师|从业24年|皮肤美容专家</div>
-        <div class="code pb-1">从业资格证编号:ZHIP1710086</div>
-        <div class="club-name">所在机构:武汉市武昌区可素美医疗美容门诊部</div>
+        <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">
-          <div class="name py-2">个人简介:</div>
-          <div class="content">
-            宋医师,医学博士,擅长痤疮(痘痘)是令很多青年男女烦恼的
-            问题,由于不够重视延误治疗或者处理不当,没有使用先进的物
-            理局部治疗,大部分人或多或少遗留了令人遗憾的瘢痕。这可怎
-            么办呢?超脉冲CO2点阵激光——痤疮瘢痕治疗金标准。清洁。
-          </div>
-        </div>
-        <div class="param px-4 pt-4">
-          <div class="name py-2">擅长:</div>
-          <div class="content">擅长痤疮(痘痘)、皮肤病。</div>
+        <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">
-            <img class="cover" src="https://picsum.photos/400/400" />
-            <div class="info">
-              <div class="name">日本水素水细胞仪</div>
-              <div class="brand mt-2">品牌:SKINREX</div>
-            </div>
-          </div>
-          <div class="device flex items-center py-4">
-            <img class="cover" src="https://picsum.photos/400/400" />
+          <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">日本水素水细胞仪</div>
-              <div class="brand mt-2">品牌:SKINREX</div>
+              <div class="name" v-text="item.equipmentName"></div>
+              <div class="brand mt-2">品牌:{{ item.brand }}</div>
             </div>
           </div>
         </div>
@@ -51,6 +49,39 @@
 <script>
 export default {
   layout: 'app',
+  data() {
+    return {
+      doctorInfo: {
+        tagList: [],
+        paramList: [],
+      },
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      const doctorInfo = localStorage.getItem('doctorInfo')
+      if (doctorInfo) {
+        this.doctorInfo = JSON.parse(doctorInfo)
+        this.fetchDetail()
+      }
+    },
+    async fetchDetail() {
+      try {
+        const res = await this.$http.api.fetchDoctorDetail({
+          doctorId: this.doctorInfo.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>
 

+ 132 - 26
pages/ph/approve/personnel/operate/index.vue

@@ -2,46 +2,125 @@
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
       <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <div class="mt-2 name">
+        <span v-text="supplierInfo.shopName"></span>
+        <span>官方认证医师</span>
+      </div>
     </div>
     <div class="page-content">
       <!-- 搜索区域 -->
       <div class="search">
-        <simple-search />
+        <SimpleSearch v-model="listQuery.doctorName" @search="onSearch" />
       </div>
       <!-- 标题 -->
-      <div class="title px-4 pt-12 pb-6">共<span>4</span>位医师</div>
+      <div class="title px-4 pt-12 pb-6">
+        共<span v-text="total"></span>位医师
+      </div>
       <!-- 列表 -->
       <div class="list">
-        <div class="section flex justify-between mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
-          <div class="info">
-            <div class="name">王医师</div>
-            <div class="code">资格证编号:ZHIP1710034</div>
-            <div class="club-name">所在机构:丽颜科美复兴广场店</div>
-          </div>
-        </div>
-        <div class="section flex justify-between mb-4">
-          <img class="cover" src="https://picsum.photos/400/400" />
+        <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">王医师</div>
-            <div class="code">资格证编号:ZHIP1710034</div>
-            <div class="club-name">所在机构:丽颜科美复兴广场店</div>
+            <div class="name" v-text="item.doctorName"></div>
+            <div class="tag">{{ item.tagList.join(' | ') }}</div>
+            <div class="code">资格证编号:{{ item.certificateNo }}</div>
+            <div class="club-name">所在机构:{{ item.clubName }}</div>
           </div>
         </div>
+        <div class="empty" v-for="i in emptyList" :key="'empty' + i"></div>
       </div>
+
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: false,
+      listQuery: {
+        appId: '',
+        doctorType: 1,
+        doctorName: '',
+        pageNum: 1,
+        pageSize: 9,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'appId']),
+    isEmpty() {
+      return this.list.length === 0
+    },
+    emptyList() {
+      return 3 - (this.list.length % 3)
+    },
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    async fetchList() {
+      try {
+        this.listQuery.appId = this.appId
+        const res = await this.$http.api.fetchDoctorList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // 搜索
+    onSearch() {
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+    // 机构详情
+    toClubDetail(item) {
+      localStorage.setItem('clubInfo', JSON.stringify({ authId: item.authId }))
+      this.$router.push('/ph/approve/club/detail')
+    },
+    // 医师详情
+    toDetail(item) {
+      localStorage.setItem('doctorInfo', JSON.stringify(item))
+      this.$router.push('/ph/approve/personnel/operate/detail')
+    },
+  },
 }
 </script>
 
 <style scoped lang="scss">
 .page {
+  position: relative;
   min-height: calc(100vh - 80px - 80px);
   background-color: #fff;
 }
@@ -49,7 +128,8 @@ export default {
 @media screen and (min-width: 768px) {
   .page-top {
     height: 420px;
-    background: url(https://static.caimei365.com/www/authentic/h5/bg-doctor.png);
+    background: url(https://static.caimei365.com/www/authentic/pc/bg-doctor.png)
+      no-repeat center;
     background-size: auto 420px;
     .logo {
       display: block;
@@ -61,13 +141,21 @@ export default {
       font-size: 30px;
       color: #fff;
     }
+
+    .logo,
+    .name {
+      transform: translateY(-30px);
+    }
   }
   .page-content {
     width: 1200px;
     margin: 0 auto;
-    position: relative;
+
     .search {
-      display: none;
+      position: absolute;
+      left: 50%;
+      top: 300px;
+      transform: translateX(-50%);
     }
 
     .title {
@@ -85,14 +173,23 @@ export default {
       flex-wrap: wrap;
       justify-content: space-between;
 
+      .empty {
+        width: 390px;
+      }
+
       .section {
         width: 390px;
-        height: 108px;
         background-color: #f3f5f6;
         border-radius: 4px;
         box-sizing: border-box;
         padding: 12px;
 
+        cursor: pointer;
+        transition: all 0.4s;
+        &:hover {
+          box-shadow: 0 0 24px rgba(0, 0, 0, 0.2);
+        }
+
         .cover {
           display: block;
           width: 84px;
@@ -106,13 +203,14 @@ export default {
             font-size: 16px;
             color: #101010;
             font-weight: bold;
-            margin-bottom: 16px;
             text-overflow: ellipsis;
             white-space: nowrap;
             overflow: hidden;
           }
+          .tag,
           .code,
           .club-name {
+            height: 24px;
             position: relative;
             font-size: 14px;
             color: #404040;
@@ -125,6 +223,10 @@ export default {
               color: #6d9eff;
             }
           }
+
+          .tag {
+            color: #909399;
+          }
         }
       }
     }
@@ -177,7 +279,6 @@ export default {
 
       .section {
         width: 93.6vw;
-        height: 26vw;
         background-color: #f3f5f6;
         border-radius: 4px;
         box-sizing: border-box;
@@ -185,8 +286,8 @@ export default {
 
         .cover {
           display: block;
-          width: 19.6vw;
-          height: 19.6vw;
+          width: 21.6vw;
+          height: 21.6vw;
         }
         .info {
           position: relative;
@@ -195,14 +296,16 @@ export default {
             font-size: 4vw;
             color: #101010;
             font-weight: bold;
-            margin-bottom: 4vw;
+            margin-bottom: 0.8vw;
             text-overflow: ellipsis;
             white-space: nowrap;
             overflow: hidden;
           }
+          .tag,
           .code,
           .club-name {
-            width: 66vw;
+            width: 62vw;
+            height: 5vw;
             position: relative;
             font-size: 3vw;
             color: #404040;
@@ -215,6 +318,9 @@ export default {
               color: #6d9eff;
             }
           }
+          .tag {
+            color: #909399;
+          }
         }
       }
     }

+ 40 - 6
pages/ph/database/article-detail.vue

@@ -1,19 +1,53 @@
 <template>
   <div class="page">
     <div class="page-top">
-      <div class="title">
-        以色列无针水光JDV/优斐斯/瑞漾小白盒/希腊YELLOW
-        ROSE专业线贵妇级除皱产品--希腊 YELLOW ROSE黄玫瑰弹力除皱精华产品
-      </div>
-      <div class="date">2022-02-16 17:42</div>
+      <div class="title" v-text="articleInfo.articleTitle"></div>
+      <div class="date">{{ articleInfo.createTime | dateFormat }}</div>
     </div>
-    <div class="page-content">一些内容</div>
+    <div class="page-content" v-html="html"></div>
   </div>
 </template>
 
 <script>
 export default {
   layout: 'app',
+  data() {
+    return {
+      articleInfo: {},
+      imageList: [],
+    }
+  },
+  computed: {
+    html() {
+      const html = this.articleInfo.articleContent
+      if (html) {
+        return html.replace(/href=/gi, '')
+      }
+      return ''
+    },
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      const articleInfo = localStorage.getItem('articleInfo')
+      if (articleInfo) {
+        this.articleInfo = JSON.parse(articleInfo)
+        this.fetchArticleDetail()
+      }
+    },
+    async fetchArticleDetail() {
+      try {
+        const res = await this.$http.api.getArticleDetail({
+          articleId: this.articleInfo.articleId,
+        })
+        this.articleInfo = { ...this.articleInfo, ...res.data }
+      } catch (error) {
+        console.log(error)
+      }
+    },
+  },
 }
 </script>
 

+ 90 - 31
pages/ph/database/article.vue

@@ -1,56 +1,115 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <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 />
+        <simple-search v-model="listQuery.articleTitle" @search="onSearch" />
       </div>
       <div class="divider"></div>
       <!-- tabbar -->
-      <simple-tabs></simple-tabs>
+      <simple-tabs
+        :tabs="tabs"
+        :current="current"
+        @change="onTabChange"
+        @search="onSearch"
+      ></simple-tabs>
       <div class="list">
-        <div class="section flex justify-between items-center">
+        <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">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及
-              希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="date">2021-07-08 17:42</div>
+            <div class="name" v-text="item.articleTitle"></div>
+            <div class="date">{{ item.createTime | dateFormat }}</div>
           </div>
-          <img class="cover" src="https://picsum.photos/200/200" />
-        </div>
-        <div class="section flex justify-between items-center">
-          <div class="info">
-            <div class="name">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及
-              希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="date">2021-07-08 17:42</div>
-          </div>
-          <img class="cover" src="https://picsum.photos/200/200" />
-        </div>
-        <div class="section flex justify-between items-center">
-          <div class="info">
-            <div class="name">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及
-              希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="date">2021-07-08 17:42</div>
-          </div>
-          <img class="cover" src="https://picsum.photos/200/200" />
+          <img class="cover" :src="item.articleImage" />
         </div>
       </div>
+
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: true,
+      tabs: tabs(),
+      current: 0,
+      listQuery: {
+        articleTitle: '',
+        authUserId: '102',
+        pageNum: 1,
+        pageSize: 4,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'supplierInfo']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    async fetchList() {
+      try {
+        this.listQuery.authUserId = this.userInfo.authUserId
+        const res = await this.$http.api.getArticleList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // 详情
+    toDetail(item) {
+      localStorage.setItem('articleInfo', JSON.stringify(item))
+      this.$router.push('/ph/database/article-detail')
+    },
+    // tab切换
+    onTabChange(item) {
+      console.log(item)
+      this.$router.push(`/ph${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.listQuery.articleTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+  },
 }
 </script>
 

+ 104 - 11
pages/ph/database/file.vue

@@ -1,36 +1,121 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <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 />
+        <simple-search v-model="listQuery.fileTitle" @search="onSearch" />
       </div>
       <div class="divider"></div>
       <!-- tabbar -->
-      <simple-tabs></simple-tabs>
+      <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">
+        <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">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及
-              希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="date">2021-07-08 17:42</div>
-            <div class="download">下载</div>
+            <div class="name" v-text="item.fileName"></div>
+            <div class="date">{{ item.createTime | dateFormat }}</div>
+            <div class="download" @click.stop="downloadLink(item)">下载</div>
           </div>
         </div>
       </div>
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+import downloadLink from '@/utils/download-link'
+
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: true,
+      tabs: tabs(),
+      current: 3,
+      listQuery: {
+        fileType: 1,
+        fileTitle: '',
+        authUserId: '',
+        pageNum: 1,
+        pageSize: 4,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'supplierInfo']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 下载方法
+    downloadLink(item) {
+      const url = `${process.env.BASE_URL}/download/file?ossName=${item.fileDownloadUrl}&fileName=${item.fileName}`
+      downloadLink(url)
+    },
+    // 预览文件
+    previewFile(item) {
+      window.open(item.filePreviewUrl)
+    },
+    // 获取列表
+    async fetchList() {
+      try {
+        this.listQuery.authUserId = this.userInfo.authUserId
+        const res = await this.$http.api.getFileList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // tab切换
+    onTabChange(item) {
+      console.log(item)
+      this.$router.push(`/ph${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.listQuery.fileTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+  },
 }
 </script>
 
@@ -82,11 +167,18 @@ export default {
       background: #fff;
       border-bottom: 1px solid #d8d8d8;
       transition: all 0.4s;
+      cursor: pointer;
 
       &:last-child {
         border-bottom: 0;
       }
 
+      &:hover {
+        .name {
+          color: #bc1724 !important;
+        }
+      }
+
       .info {
         width: 100%;
         position: relative;
@@ -99,6 +191,7 @@ export default {
           margin-bottom: 18px;
           text-align: justify;
           @include ellipsis(2);
+          transition: all 0.4s;
         }
         .date {
           font-size: 18px;

+ 104 - 26
pages/ph/database/image.vue

@@ -1,55 +1,132 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <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 />
+        <simple-search v-model="listQuery.imageTitle" @search="onSearch" />
       </div>
       <div class="divider"></div>
       <!-- tabbar -->
-      <simple-tabs></simple-tabs>
+      <simple-tabs
+        :tabs="tabs"
+        :current="current"
+        @change="onTabChange"
+        @search="onSearch"
+      ></simple-tabs>
       <div class="list">
-        <div class="section">
+        <div class="section" v-for="item in list" :key="item.imageId">
           <div class="info">
-            <div class="name">
-              VISIA第七代皮肤图像检测分析系统皮肤检测以色列无针水光JDV/优斐斯/瑞漾小白盒希腊YELLOWROSE,无针透皮注入
+            <div class="name" v-text="item.imageTitle"></div>
+            <div class="date">{{ item.createTime | dateFormat }}</div>
+            <div class="download" @click="downloadLink(item.imageZipUrl)">
+              保存所有图片
             </div>
-            <div class="date">2022-02-16 17:42</div>
-            <div class="download">保存所有图片</div>
           </div>
           <div class="images grid grid-cols-4 md:grid-cols-8 gap-3 md:gap-4">
-            <div class="item">
-              <img
-                class="object-cover"
-                src="https://picsum.photos/200/400?random=1"
-              />
-            </div>
-            <div class="item">
-              <img
-                class="object-cover"
-                src="https://picsum.photos/800/400?random=1"
-              />
-            </div>
-            <div class="item">
-              <img
-                class="object-cover"
-                src="https://picsum.photos/60/400?random=1"
-              />
+            <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>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+import downloadLink from '@/utils/download-link'
+import { ImagePreview } from 'vant'
+
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: true,
+      tabs: tabs(),
+      current: 1,
+      listQuery: {
+        imageTitle: '',
+        authUserId: '',
+        pageNum: 1,
+        pageSize: 4,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'supplierInfo']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 下载方法
+    downloadLink,
+    // 获取列表
+    async fetchList() {
+      try {
+        this.listQuery.authUserId = this.userInfo.authUserId
+        const res = await this.$http.api.getImageList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // 图片预览
+    onImagePreview(imageList, index) {
+      ImagePreview({
+        images: imageList,
+        startPosition: index,
+        loop: true,
+        showIndex: true,
+        showIndicators: true,
+        closeable: true,
+      })
+    },
+    // tab切换
+    onTabChange(item) {
+      console.log(item)
+      this.$router.push(`/ph${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.listQuery.imageTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+  },
 }
 </script>
 
@@ -140,6 +217,7 @@ export default {
             display: block;
             width: 100%;
             height: 100%;
+            cursor: pointer;
           }
         }
       }

+ 91 - 9
pages/ph/database/package.vue

@@ -1,34 +1,116 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <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 />
+        <simple-search v-model="listQuery.fileTitle" @search="onSearch" />
       </div>
       <div class="divider"></div>
       <!-- tabbar -->
-      <simple-tabs></simple-tabs>
+      <simple-tabs
+        :tabs="tabs"
+        :current="current"
+        @change="onTabChange"
+        @search="onSearch"
+      ></simple-tabs>
       <div class="list">
-        <div class="section flex justify-between items-center">
+        <div
+          class="section flex justify-between items-center"
+          v-for="item in list"
+          :key="item.fileId"
+        >
           <div class="info">
-            <div class="name">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="download">点击下载</div>
+            <div class="name" v-text="item.fileName"></div>
+            <div class="download" @click="downloadLink(item)">点击下载</div>
           </div>
         </div>
       </div>
+
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+import downloadLink from '@/utils/download-link'
+
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: true,
+      tabs: tabs(),
+      current: 4,
+      listQuery: {
+        fileType: 2,
+        fileTitle: '',
+        authUserId: '',
+        pageNum: 1,
+        pageSize: 4,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'supplierInfo']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 下载方法
+    downloadLink(item) {
+      const url = `${process.env.BASE_URL}/download/file?ossName=${item.fileDownloadUrl}&fileName=${item.fileName}`
+      downloadLink(url)
+    },
+    // 获取列表
+    async fetchList() {
+      try {
+        this.listQuery.authUserId = this.userInfo.authUserId
+        const res = await this.$http.api.getFileList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // tab切换
+    onTabChange(item) {
+      console.log(item)
+      this.$router.push(`/ph${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.listQuery.fileTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+  },
 }
 </script>
 

+ 92 - 12
pages/ph/database/video.vue

@@ -1,37 +1,117 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <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 />
+        <simple-search v-model="listQuery.videoTitle" @search="onSearch" />
       </div>
       <div class="divider"></div>
       <!-- tabbar -->
-      <simple-tabs></simple-tabs>
+      <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">
+        <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">
-              以色列无针水光JDV/优斐斯/瑞漾小白盒及
-              希腊YELLOWROSE,无针透皮注入技...
-            </div>
-            <div class="date">2021-07-08 17:42</div>
-            <div class="download">保存视频</div>
+            <div class="name" v-text="item.videoTitle"></div>
+            <div class="date">{{ item.createTime | dateFormat }}</div>
+            <div class="download" @click="downloadLink(item)">保存视频</div>
           </div>
-          <img class="cover" src="https://picsum.photos/200/200" />
+          <video class="cover" :src="item.videoPreviewUrl"></video>
         </div>
       </div>
+
+      <!-- 列表为空 -->
+      <SimpleEmpty
+        v-if="!total && !isRequest"
+        description="敬请期待~"
+      ></SimpleEmpty>
+      <!-- 页码 -->
+      <SimplePagination
+        v-if="total > listQuery.pageSize"
+        :total="total"
+        :pageItems="listQuery.pageSize"
+        @change="onPagiantionChange"
+      ></SimplePagination>
     </div>
   </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { tabs } from '@/configs/tabs'
+import downloadLink from '@/utils/download-link'
+
 export default {
   layout: 'app',
+  data() {
+    return {
+      isRequest: true,
+      tabs: tabs(),
+      current: 2,
+      listQuery: {
+        videoTitle: '',
+        authUserId: '102',
+        pageNum: 1,
+        pageSize: 4,
+      },
+      list: [],
+      total: 0,
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'supplierInfo']),
+  },
+  mounted() {
+    this.fetchList()
+  },
+  methods: {
+    // 下载方法
+    downloadLink(item) {
+      const url = `${process.env.BASE_URL}/download/file?ossName=${item.videoDownloadUrl}&fileName=${item.videoName}`
+      downloadLink(url)
+    },
+    // 获取列表
+    async fetchList() {
+      try {
+        this.listQuery.authUserId = this.userInfo.authUserId
+        const res = await this.$http.api.getVideoList(this.listQuery)
+        this.list = res.data.list
+        this.total = res.data.total
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.isRequest = false
+      }
+    },
+    // tab切换
+    onTabChange(item) {
+      console.log(item)
+      this.$router.push(`/ph${item.path}`)
+    },
+    // 搜索
+    onSearch(keyword) {
+      this.listQuery.videoTitle = keyword
+      this.listQuery.pageNum = 1
+      this.fetchList()
+    },
+    // 页码变化
+    onPagiantionChange(index) {
+      this.listQuery.pageNum = index
+      this.fetchList()
+    },
+  },
 }
 </script>
 

+ 126 - 3
pages/ph/feedback/index.vue

@@ -1,22 +1,75 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <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">提交</div>
+      <div class="submit mt-6" @click="onSubmit">提交</div>
     </div>
+
+    <van-dialog v-model="showModal" class="dialog" @confirm="onConfirm">
+      <div class="dialog-content">
+        <img
+          class="feedback-icon"
+          :src="$store.getters.static + '/icon-submit-success.png'"
+        />
+        <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',
+  data() {
+    return {
+      content: '',
+      showModal: false,
+    }
+  },
+  computed: {
+    ...mapGetters(['supplierInfo', 'userInfo']),
+    isEmpty() {
+      return this.content.length === 0
+    },
+  },
+  async created() {
+    try {
+      await this.$http.api.checkToken()
+    } catch (error) {
+      console.log(error)
+    }
+  },
+  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.replace('/ph')
+    },
+  },
 }
 </script>
 
@@ -78,6 +131,45 @@ export default {
       }
     }
   }
+
+  .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;
+      }
+
+      .feedback-icon {
+        width: 140px;
+      }
+    }
+  }
 }
 
 // 移动 端
@@ -124,5 +216,36 @@ export default {
       }
     }
   }
+
+  .feedback-icon {
+    display: block;
+    width: 26vw;
+  }
+
+  .dialog {
+    width: 76vw;
+    border-radius: 0;
+
+    .dialog-content {
+      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;
+      }
+    }
+  }
 }
 </style>

+ 12 - 2
pages/ph/index.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="page">
     <div class="page-top flex flex-col justify-center items-center">
-      <img class="logo" src="https://picsum.photos/200/200" />
-      <span class="name mt-2">上海品辉医疗科技有限公司</span>
+      <img class="logo" :src="supplierInfo.logo" />
+      <div class="name mt-2" v-text="supplierInfo.shopName"></div>
     </div>
     <div class="page-content">
       <keep-alive>
@@ -30,6 +30,7 @@
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
 export default {
   layout: 'app',
   data() {
@@ -67,9 +68,18 @@ export default {
       ],
     }
   },
+  computed: {
+    ...mapGetters(['supplierInfo']),
+  },
   created() {},
   methods: {
     toDetail(item) {
+      const hasLogin = this.$store.getters.accessToken
+      if (item.id > 0 && !hasLogin) {
+        this.$toast({ message: '请先登录', duration: 1000 })
+        this.$store.commit('app/SHOW_LOGIN')
+        return
+      }
       this.$router.push(item.path)
     },
     onMouseover(item) {

+ 20 - 2
plugins/axios.js

@@ -1,8 +1,17 @@
+// 引入接口
+import initApi from '@/apis/index'
+import Vue from 'vue'
+import { Dialog, Toast } from 'vant'
+
 export default function (context) {
   const { $axios, redirect, store } = context
 
+  // 初始化接口及挂载接口
+  const apiMap = initApi($axios)
+  Vue.prototype.$http = context.$http = { api: apiMap }
+
   $axios.onRequest((config) => {
-    $axios.setHeader('X-Token', 'abc')
+    $axios.setHeader('X-Token', store.getters.accessToken)
   })
 
   // 响应拦截
@@ -16,7 +25,16 @@ export default function (context) {
     }
     // 登录过期
     if (res.code === -99) {
-      console.log('登录过期')
+      const result = await Dialog.alert({
+        title: '提示',
+        message: '登录已过期,请重新登录',
+        theme: 'round-button',
+        confirmButtonColor: 'linear-gradient(to left, #404040, #101010)',
+      })
+      if (result === 'confirm') {
+        const path = '/' + store.getters.type
+        redirect(path)
+      }
     }
     return Promise.reject(res)
   })

+ 2 - 0
plugins/vant.js

@@ -1,5 +1,7 @@
 import Vue from 'vue'
 import Vant from 'vant'
+import '@vant/touch-emulator'
+
 import { Toast, Dialog, Notify } from 'vant'
 
 Vue.prototype.$toast = Toast

+ 7 - 0
store/getters.js

@@ -2,4 +2,11 @@ export default {
   isPc: (state) => state.app.isPc,
   screen: (state) => state.app.screen,
   static: (state) => state.app.static,
+  loginVisiable: (state) => state.app.loginVisiable,
+  userInfo: (state) => state.user.userInfo,
+  accessToken: (state) => state.user.accessToken,
+  appId: (state) => state.user.appId,
+  accountType: (state) => state.user.accountType,
+  type: (state) => state.user.type,
+  supplierInfo: (state) => state.supplier.supplierInfo,
 }

+ 18 - 0
store/supplier.js

@@ -0,0 +1,18 @@
+const state = () => ({
+  supplierInfo: {},
+})
+
+const mutations = {
+  SET_SUPPLIER_INFO(state, supplierInfo) {
+    state.supplierInfo = supplierInfo
+  },
+}
+
+const actions = {}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+}

+ 49 - 0
store/user.js

@@ -0,0 +1,49 @@
+import { setCookies, removeCookies } from '@/utils/auth'
+
+const state = () => ({
+  userInfo: {},
+  accessToken: '',
+  appId: '',
+  accountType: '',
+  type: '',
+})
+
+const mutations = {
+  SET_USERINFO(state, data) {
+    state.userInfo = data
+    state.accessToken = data.accessToken
+    setCookies('userInfo', JSON.stringify(data))
+    setCookies('accessToken', data.accessToken)
+  },
+  SET_APPID(state, appId) {
+    state.appId = appId
+    setCookies('appId', appId)
+  },
+  SET_TYPE(state, type) {
+    state.type = type
+  },
+  SET_ACCOUNT_TYPE(state, accountType) {
+    state.accountType = accountType
+    setCookies('accountType', accountType)
+  },
+}
+
+const actions = {
+  // 退出登录
+  logout({ commit }) {
+    commit('SET_USERINFO', {})
+    removeCookies('userInfo')
+    removeCookies('accessToken')
+  },
+  // 用户登录
+  login({ commit }, data) {
+    commit('SET_USERINFO', data)
+  },
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+}

+ 13 - 0
utils/auth.js

@@ -0,0 +1,13 @@
+import Cookies from 'js-cookie'
+
+export function getCookies(key) {
+  return Cookies.get(key)
+}
+
+export function setCookies(key, value) {
+  return Cookies.set(key, value)
+}
+
+export function removeCookies(key) {
+  return Cookies.remove(key)
+}

+ 32 - 0
utils/clipboard.js

@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import Clipboard from 'clipboard'
+
+function clipboardSuccess(message) {
+  Vue.prototype.$toast({
+    message,
+    type: 'success',
+    duration: 1500
+  })
+}
+
+function clipboardError(message) {
+  Vue.prototype.$toast({
+    message: '复制失败',
+    type: 'fail'
+  })
+}
+
+export default function handleClipboard(text, event, message) {
+  const clipboard = new Clipboard(event.target, {
+    text: () => text
+  })
+  clipboard.on('success', () => {
+    clipboardSuccess(message)
+    clipboard.destroy()
+  })
+  clipboard.on('error', () => {
+    clipboardError()
+    clipboard.destroy()
+  })
+  clipboard.onClick(event)
+}

+ 12 - 0
utils/download-link.js

@@ -0,0 +1,12 @@
+import handleClipboard from '@/utils/clipboard'
+import { isWeChat } from '@/utils/validator'
+import { downloadUrlLink } from '@/utils/index'
+
+// 通过链接下载
+export default function downloadLink(link) {
+  if (isWeChat()) {
+    return handleClipboard(link, $event, '下载链接已复制到剪切板')
+  } else {
+    downloadUrlLink(link)
+  }
+}

+ 1 - 0
utils/index.js

@@ -120,6 +120,7 @@ export function downloadUrlLink(url) {
   const a = document.createElement('a')
   a.setAttribute('download', true)
   a.setAttribute('href', url)
+  a.setAttribute('target', '_blank')
   a.style.display = 'none'
   document.body.appendChild(a)
   a.click()

+ 66 - 0
utils/map-utils.js

@@ -0,0 +1,66 @@
+function mapInit(callback) {
+  function loadScript() {
+    var script = document.createElement('script')
+    script.src =
+      'https://api.map.baidu.com/api?v=1.0&type=webgl&ak=vsIfSztpPmCtmBRfRiIAM57hbxBQbmgQ&callback=initMapApi'
+    document.body.appendChild(script)
+  }
+  if (!window.BMapGL) {
+    window.initMapApi = callback
+    loadScript()
+  } else {
+    callback()
+  }
+}
+
+// 定位当前位置
+export function loactionSelf() {
+  return new Promise((resolve, reject) => {
+    mapInit(() => {
+      const BMapGL = window.BMapGL
+      var geolocation = new BMapGL.Geolocation({
+        // 是否使用高精度定位,默认:true
+        enableHighAccuracy: true,
+        // 设置定位超时时间,默认:无穷大
+        timeout: 10000,
+      })
+      // 开启SDK辅助定位
+      geolocation.enableSDKLocation()
+      geolocation.getCurrentPosition(function (r) {
+        // alert(JSON.stringify(r))
+        if (this.getStatus() === 0) {
+          // alert(JSON.stringify(r))
+          resolve({
+            point: r.point,
+            address: r.address,
+          })
+        } else {
+          reject('failed' + this.getStatus())
+        }
+      })
+    })
+  })
+}
+
+// 地址导航
+export function mapNavigate(options = {}, origin) {
+  console.log(options)
+  // 百度
+  if (origin === 'baidu') {
+    console.log('百度地图')
+    options.locationUrl = `http://api.map.baidu.com/marker?location=${options.lat},${options.lng}&title=${options.title}&content=${options.address}&output=html&src=webapp.baidu.openAPIdemo`
+  }
+  // 腾讯
+  if (origin === 'tx') {
+    console.log('腾讯地图')
+    options.locationUrl = `https://apis.map.qq.com/uri/v1/marker?marker=coord:${options.lat},${options.lng};${options.title};addr:${options.address}&referer=BWUBZ-LRLCQ-JON5T-GJLC4-URIMQ-CRBO6`
+  }
+  // 高德
+  if (origin === 'gaode') {
+    console.log('高德地图')
+    options.locationUrl = `https://uri.amap.com/marker?position=${options.lng},${options.lat}&name=${options.title}&coordinate=gaode&callnative=0`
+  }
+
+  // window.open(options.locationUrl)
+  window.location.href = options.locationUrl
+}

+ 15 - 0
utils/validator.js

@@ -0,0 +1,15 @@
+// 校验手机号是否合法
+export function isMobile(arg) {
+  const reg = /^1[3456789]\d{9}$/
+  return reg.test(arg)
+}
+// 判断浏览器环境是否为微信
+export function isWeChat() {
+  // window.navigator.userAgent属性包含了浏览器类型、版本、操作系统类型、浏览器引擎类型等信息,这个属性可以用来判断浏览器类型
+  var ua = window.navigator.userAgent.toLowerCase()
+  // 通过正则表达式匹配ua中是否含有MicroMessenger字符串
+  return (
+    ua.match(/MicroMessenger/i) &&
+    ua.match(/MicroMessenger/i).includes('micromessenger')
+  )
+}