Переглянути джерело

Merge branch 'develop/3.2.1' into pre

# Conflicts:
#	src/utils/request.js
Damon 11 місяців тому
батько
коміт
13b2877780
77 змінених файлів з 8944 додано та 1114 видалено
  1. 7 0
      .env.development
  2. 7 0
      .env.production
  3. 7 0
      .env.test
  4. 2880 3
      package-lock.json
  5. 6 1
      package.json
  6. 0 4
      public/index.html
  7. 25 10
      src/App.vue
  8. 45 0
      src/api/activity.js
  9. 10 0
      src/api/agreement.js
  10. 28 0
      src/api/channel.js
  11. 10 0
      src/api/content.js
  12. 10 0
      src/api/devices.js
  13. 1 1
      src/api/guide.js
  14. 32 0
      src/api/help.js
  15. 9 0
      src/api/imusic.js
  16. 26 0
      src/api/map.js
  17. 67 0
      src/api/pay.js
  18. 10 0
      src/api/ximalaya.js
  19. 8 3
      src/common/button.scss
  20. 14 0
      src/common/main.scss
  21. 84 0
      src/components/my-audio/my-audio.vue
  22. 107 0
      src/components/my-list/my-list.vue
  23. 4 35
      src/components/open-app/open-app.vue
  24. 1 1
      src/components/uni-popup/uni-popup.vue
  25. 0 148
      src/components/uni-segmented-control/uni-segmented-control.vue
  26. 576 0
      src/components/uv-parse/node/node.vue
  27. 1335 0
      src/components/uv-parse/parser.js
  28. 498 0
      src/components/uv-parse/uv-parse.vue
  29. 10 36
      src/main.js
  30. 83 86
      src/manifest.json
  31. 217 109
      src/pages.json
  32. 140 0
      src/pages/activity/index.vue
  33. 127 0
      src/pages/activity/new.vue
  34. 41 0
      src/pages/agreement/index.vue
  35. 10 10
      src/pages/article/index.vue
  36. 3 5
      src/pages/about/index.vue
  37. 216 0
      src/pages/channel/index.vue
  38. 214 0
      src/pages/content/index.vue
  39. 45 0
      src/pages/devices/detail.vue
  40. 14 6
      src/pages/guide/index.vue
  41. 130 0
      src/pages/help/detail.vue
  42. 219 5
      src/pages/help/index.vue
  43. 122 0
      src/pages/imusic/index.vue
  44. 2 2
      src/pages/lottery/detail.vue
  45. 9 9
      src/pages/lottery/index.vue
  46. 568 0
      src/pages/map/index.vue
  47. 1 1
      src/pages/museum/img.json
  48. 97 0
      src/pages/pay/detail.vue
  49. 560 0
      src/pages/pay/index.vue
  50. 0 22
      src/pages/privacy/agreement.vue
  51. 4 4
      src/pages/share/channels.vue
  52. 13 42
      src/pages/share/controls.vue
  53. 19 16
      src/pages/share/detail.vue
  54. 31 20
      src/pages/share/list.vue
  55. 0 153
      src/pages/user/agreement.vue
  56. 0 375
      src/pages/vip/index.vue
  57. 24 0
      src/pages/warning/index.vue
  58. 30 0
      src/pages/ximalaya/error.vue
  59. 54 0
      src/pages/ximalaya/index.vue
  60. 30 0
      src/pages/ximalaya/success.vue
  61. BIN
      src/static/address.png
  62. BIN
      src/static/location.png
  63. BIN
      src/static/museum/detail/4.png
  64. BIN
      src/static/museum/detail/5.png
  65. BIN
      src/static/museum/detail/6.png
  66. BIN
      src/static/pay/bg.png
  67. BIN
      src/static/pay/bigbg.png
  68. BIN
      src/static/pay/light.png
  69. BIN
      src/static/pay/mask.png
  70. BIN
      src/static/phone.png
  71. BIN
      src/static/play.png
  72. BIN
      src/static/share/empty.png
  73. BIN
      src/static/stop.png
  74. 39 0
      src/utils/openApp.js
  75. 40 0
      src/utils/parseTime.js
  76. 4 5
      src/utils/request.js
  77. 21 2
      vue.config.js

+ 7 - 0
.env.development

@@ -0,0 +1,7 @@
+NODE_ENV = development
+
+# 开发环境配置
+VUE_APP_ENV = 'development'
+
+# 开发环境接口
+VUE_APP_BASE_API = 'https://o3tapi.radio1964.com/web'

+ 7 - 0
.env.production

@@ -0,0 +1,7 @@
+NODE_ENV = production
+
+# 生产环境配置
+VUE_APP_ENV = 'production'
+
+# 生产环境接口
+VUE_APP_BASE_API = 'https://client.ohplay.radio1964.net/web'

+ 7 - 0
.env.test

@@ -0,0 +1,7 @@
+NODE_ENV = production
+
+# 测试环境配置
+VUE_APP_ENV = 'test'
+
+# 测试环境接口
+VUE_APP_BASE_API = 'https://o3tapi.radio1964.com/web'

Різницю між файлами не показано, бо вона завелика
+ 2880 - 3
package-lock.json


+ 6 - 1
package.json

@@ -3,7 +3,8 @@
   "version": "0.1.0",
   "version": "0.1.0",
   "private": true,
   "private": true,
   "scripts": {
   "scripts": {
-    "serve": "npm run dev:h5",
+    "dev": "npm run dev:h5",
+    "test": "vue-cli-service build --mode test",
     "build": "npm run build:h5",
     "build": "npm run build:h5",
     "build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
     "build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
     "build:custom": "cross-env NODE_ENV=production uniapp-cli custom",
     "build:custom": "cross-env NODE_ENV=production uniapp-cli custom",
@@ -48,6 +49,7 @@
     "test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i"
     "test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
     "@dcloudio/uni-app-plus": "^2.0.1-34920220630001",
     "@dcloudio/uni-app-plus": "^2.0.1-34920220630001",
     "@dcloudio/uni-h5": "^2.0.1-34920220630001",
     "@dcloudio/uni-h5": "^2.0.1-34920220630001",
     "@dcloudio/uni-helper-json": "*",
     "@dcloudio/uni-helper-json": "*",
@@ -70,9 +72,12 @@
     "@lucky-canvas/vue": "^0.1.11",
     "@lucky-canvas/vue": "^0.1.11",
     "@vue/shared": "^3.0.0",
     "@vue/shared": "^3.0.0",
     "@zebra-ui/swiper": "^2.2.2",
     "@zebra-ui/swiper": "^2.2.2",
+    "axios": "^1.4.0",
     "core-js": "^3.6.5",
     "core-js": "^3.6.5",
     "flyio": "^0.6.2",
     "flyio": "^0.6.2",
+    "js-base64": "^3.7.5",
     "regenerator-runtime": "^0.12.1",
     "regenerator-runtime": "^0.12.1",
+    "vconsole": "^3.15.1",
     "vue": "^2.6.11",
     "vue": "^2.6.11",
     "vuex": "^3.2.0"
     "vuex": "^3.2.0"
   },
   },

+ 0 - 4
public/index.html

@@ -11,10 +11,6 @@
         var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
         var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
         document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
         document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
     </script>
     </script>
-    <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
-    <script>
-        var vConsole = new window.VConsole();
-    </script>
     <link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
     <link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
 </head>
 </head>
 
 

+ 25 - 10
src/App.vue

@@ -4,18 +4,33 @@ export default {
     // 用户信息
     // 用户信息
     userInfo: {},
     userInfo: {},
     // 状态栏
     // 状态栏
-    statusBarHeight: 20
+    statusBarHeight: 20,
+    // 设备信息
+    deviceInfo: {},
+    // 是否在App内部H5
+    inside: false
   },
   },
   onLaunch() {
   onLaunch() {
-    // 获取App方法
-    getUserInfo.postMessage('获取用户信息')
-    getDeviceInfo.postMessage('获取设备信息')
-    // 暴露setUserInfo方法给APP
-    window['setUserInfo'] = res => {
-      this.globalData.userInfo = JSON.parse(res)
-    }
-    window['setDeviceInfo'] = res => {
-      this.globalData.statusBarHeight = JSON.parse(res).statusHeight
+    try {
+      // 获取App方法
+      getUserInfo.postMessage('获取用户信息')
+      getDeviceInfo.postMessage('获取设备信息')
+      getMWDeviceInfo.postMessage('获取音响信息')
+      // 在App内部H5
+      this.globalData.inside = true
+      // 暴露setUserInfo方法给APP
+      window['setUserInfo'] = res => {
+        this.globalData.userInfo = JSON.parse(res)
+      }
+      window['setDeviceInfo'] = res => {
+        this.globalData.statusBarHeight = JSON.parse(res).statusHeight
+      }
+      window['setMWDeviceInfo'] = res => {
+        this.globalData.deviceInfo = JSON.parse(res)
+      }
+    } catch {
+      // 在浏览器H5
+      this.globalData.inside = false
     }
     }
   }
   }
 }
 }

+ 45 - 0
src/api/activity.js

@@ -0,0 +1,45 @@
+import request from '@/utils/request'
+const header = getApp().globalData.userInfo
+
+// 联通流量兑换活动详情
+export function newDetail(data) {
+  return request({
+    url: `/admin/activity/addOrUpdate`,
+    header,
+    method: 'post',
+    data
+  })
+}
+
+// 活动详情
+export function detail(data) {
+  return request({
+    url: `/serviceActivity/getDetailInfo`,
+    header,
+    method: 'get',
+    data
+  })
+}
+
+// 一般立即兑换
+export function submit(data) {
+  return request({
+    url: `/serviceActivity/doExchange`,
+    header,
+    method: 'post',
+    data
+  })
+}
+
+// 新活动(联通流量免费赠送)
+export function doNewActivity(data) {
+  return request({
+    // 设置请求头
+    header: {
+      'content-type': 'application/x-www-form-urlencoded'
+    },
+    url: `/order/4GActivity/doExchange`,
+    method: 'post',
+    data
+  })
+}

+ 10 - 0
src/api/agreement.js

@@ -0,0 +1,10 @@
+import request from "../utils/request";
+
+// 详情
+export function detail(data) {
+  return request({
+    url: `/agreement/getInfo`,
+    method: 'get',
+    data
+  })
+}

+ 28 - 0
src/api/channel.js

@@ -0,0 +1,28 @@
+import request from "../utils/request";
+
+// 列表
+export function list(data) {
+  return request({
+    url: `/Ohplay/Device/CustomBroadcast/pageList`,
+    method: "post",
+    data,
+  });
+}
+
+// 新增
+export function submit(data){
+  return request({
+    url: `/Ohplay/Device/CustomBroadcast/batchAdd`,
+    method: 'post',
+    data
+  })
+}
+
+// 删除
+export function remove(data) {
+  return request({
+    url: `/Ohplay/Device/CustomBroadcast/delete`,
+    method: 'post',
+    data
+  })
+}

+ 10 - 0
src/api/content.js

@@ -0,0 +1,10 @@
+import request from "../utils/request";
+
+// 详情
+export function detail(data) {
+  return request({
+    url: `/contentShare/detail`,
+    method: 'get',
+    data
+  })
+}

+ 10 - 0
src/api/devices.js

@@ -0,0 +1,10 @@
+import request from "../utils/request";
+
+// 获取设备操作指南
+export function detail(data) {
+  return request({
+    url: `/device/getOperateGuide`,
+    method: 'get',
+    data
+  })
+}

+ 1 - 1
src/api/guide.js

@@ -1,6 +1,6 @@
 import request from "../utils/request"
 import request from "../utils/request"
 
 
-export function getContent(data) {
+export function detail(data) {
   return request({
   return request({
     url: `/device/getGuidePageContent`,
     url: `/device/getGuidePageContent`,
     method: 'get',
     method: 'get',

+ 32 - 0
src/api/help.js

@@ -0,0 +1,32 @@
+import request from "../utils/request";
+const header = getApp().globalData.userInfo
+
+// 反馈类型列表
+export function list() {
+  return request({
+    url: `/user/opinions/typeList`,
+    header,
+    method: 'get'
+  })
+}
+
+// 提交
+export function submit(data) {
+  return request({
+    url: `/user/opinions/addOpinions`,
+    header,
+    method: 'post',
+    data
+  })
+}
+
+// 我的反馈
+
+export function detail(data) {
+  return request({
+    url: `/user/opinions/myOpinionsList`,
+    header,
+    method: 'get',
+    data
+  })
+}

+ 9 - 0
src/api/imusic.js

@@ -0,0 +1,9 @@
+import request from "../utils/request";
+
+export function detail(data){
+  return request({
+    url: `/music/atmd/groupDetail`,
+    method: 'post',
+    data
+  })
+}

+ 26 - 0
src/api/map.js

@@ -0,0 +1,26 @@
+import request from "../utils/request";
+
+// 省市区
+export function options() {
+  return request({
+    url: `/province/city/area`,
+    method: 'get'
+  })
+}
+
+// 线下店铺
+export function list(data) {
+  return request({
+    url: `/offline/store/list`,
+    method: 'get',
+    data
+  })
+}
+
+// 第三方店铺
+export function storeList() {
+  return request({
+    url: `/offline/store/recommendList`,
+    method: 'get'
+  })
+}

+ 67 - 0
src/api/pay.js

@@ -0,0 +1,67 @@
+import request from "@/utils/request";
+
+// openId
+export function openId(data) {
+  return request({
+    url: `/wxPay/wapAuth?code=${data}`,
+    method: "post"
+  });
+}
+
+// 一般流量购买详情
+export function detail(data) {
+  return request({
+    url: `/order/4G/cmpForward`,
+    method: 'get',
+    data
+  })
+}
+
+// 流量套餐
+export function options(data) {
+  return request({
+    url: `/order/4G/dataPlanList`,
+    method: 'get',
+    data
+  })
+}
+
+// 流量列表
+export function order(data) {
+  return request({
+    url: `/order/4G/deviceTrafficOrderList`,
+    method: 'get',
+    data
+  })
+}
+
+// 下单
+export function wechatPay(data) {
+  return request({
+    url: `/wxPay/orderCreate`,
+    method: 'post',
+    data
+  })
+}
+
+// 充值记录
+export function list(data) {
+  return request({
+    url: `/order/4G/orderGet`,
+    mthod: 'get',
+    data
+  })
+}
+
+// 联通流量详细信息 
+export function newAcitivityDetail(data) {
+  return request({
+    // 设置请求头
+    header: {
+      'content-type': 'application/x-www-form-urlencoded'
+    },
+    url: `/order/4GActivity/getDetailInfo`,
+    method: 'post',
+    data: data,
+  })
+}

+ 10 - 0
src/api/ximalaya.js

@@ -0,0 +1,10 @@
+import request from "@/utils/request"
+
+// 回调接口
+export function getCallBack(data) {
+  return request({
+    url: `/callback/ximalaya/reportAuthInfo`,
+    method: 'get',
+    data
+  })
+}

+ 8 - 3
src/common/button.scss

@@ -1,9 +1,9 @@
-button{
+button {
   height: 100rpx;
   height: 100rpx;
   line-height: 100rpx;
   line-height: 100rpx;
 }
 }
 
 
-button::after{
+button::after {
   content: none
   content: none
 }
 }
 
 
@@ -12,11 +12,16 @@ uni-button[type=submit] {
   color: #FFF;
   color: #FFF;
 }
 }
 
 
+uni-button[type=buy] {
+  background-image: linear-gradient(#F3CF97, #F6E5C4);
+  color: #7C541A;
+}
+
 uni-button[circle] {
 uni-button[circle] {
   border-radius: 50rpx;
   border-radius: 50rpx;
 }
 }
 
 
-uni-slider{
+uni-slider {
   margin: 0;
   margin: 0;
   width: 100%;
   width: 100%;
 }
 }

+ 14 - 0
src/common/main.scss

@@ -16,6 +16,10 @@ view[flex]{
   display: flex;
   display: flex;
 }
 }
 
 
+view[column] {
+  flex-direction: column;
+}
+
 view[wrap]{
 view[wrap]{
   flex-wrap: wrap;
   flex-wrap: wrap;
 }
 }
@@ -61,4 +65,14 @@ textarea{
 
 
 textarea{
 textarea{
   padding: 30rpx;
   padding: 30rpx;
+}
+
+uni-modal{
+  color: #000;
+}
+
+.owt{
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
 }
 }

+ 84 - 0
src/components/my-audio/my-audio.vue

@@ -0,0 +1,84 @@
+<template>
+  <!-- 播放器 -->
+  <view class="audio" :style="{ 'background': (bg ? `url(${bg}) no-repeat 100% / 100%` : '#EEE') }">
+    <image class="pic" :src="pic" />
+    <view class="info">
+      <view class="song">{{ name }}</view>
+      <view class="singer">{{ singer ? singer : '暂无歌手名' }}</view>
+    </view>
+    <image v-if="newStatus !== 1" class="btn" src="../../static/play.png" @click="getPlay" />
+    <image v-else class="btn" src="../../static/stop.png" @click="getPlay" />
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    bg: String,
+    pic: String,
+    name: String,
+    status: Number,
+    singer: String
+  },
+  data() {
+    return {
+      newStatus: this.status
+    }
+  },
+  watch: {
+    status(val) {
+      this.newStatus = val
+    }
+  },
+  methods: {
+    getPlay() {
+      this.$emit('click')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.audio {
+  width: 100%;
+  height: 104px;
+  padding: 8px 16px 8px 8px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  font-weight: bold;
+
+  .pic {
+    width: 88px;
+    height: 88px;
+    border-radius: 8px;
+    margin-right: 16px;
+  }
+
+  .info {
+    width: calc(100% - 144px);
+    color: #000;
+
+    .song {
+      font-size: 16px;
+      margin-bottom: 16px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+
+    .singer {
+      font-size: 12px;
+      opacity: 0.6;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+  }
+
+  .btn {
+    width: 40px;
+    height: 40px;
+  }
+}
+</style>

+ 107 - 0
src/components/my-list/my-list.vue

@@ -0,0 +1,107 @@
+<template>
+  <view class="box">
+    <view class="info">
+      <view class="name">{{ name }}</view>
+      <button @click="getPlay">收听{{ audioOption[type] }}</button>
+    </view>
+    <view class="list">
+      <ol v-if="data.childList.length > 0">
+        <li v-for="(item, index) in data.childList.slice(0, 4)" :key="item.id">{{ index + 1 }}、{{ item.audioName }}</li>
+      </ol>
+      <view v-else>{{ audioOption[type] }}暂无内容</view>
+    </view>
+    <img class="pic" :src="pic" />
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    pic: String,
+    name: String,
+    type: Number,
+    data: Object
+  },
+  data() {
+    return {
+      audioOption: {
+        8: '播客专辑',
+        10: '歌单',
+        15: '音乐专辑'
+      }
+    }
+  },
+  methods: {
+    getPlay() {
+      this.$emit('click')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.box {
+  width: 100%;
+  height: 203px;
+  padding: 18px 16px 24px;
+  background: #EEE;
+  border-radius: 8px;
+  color: #000;
+  position: relative;
+
+  .info {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .name {
+      flex: 1;
+      font-weight: bold;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis
+    }
+
+    button {
+      height: auto;
+      line-height: 1;
+      padding: 14px 28px;
+      font-size: 16px;
+      font-weight: bold;
+      color: #FFF;
+      border-radius: 25px;
+      background: linear-gradient(146deg, #000000 0%, #333333 100%);
+    }
+  }
+
+  .list {
+    display: flex;
+    justify-content: space-between;
+    position: relative;
+    font-size: 14px;
+    font-weight: bold;
+
+    ol {
+      padding-inline-start: 18px;
+      padding: 0;
+
+      li {
+        width: 140px;
+        margin-bottom: 12px;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
+  }
+
+  .pic {
+    position: absolute;
+    right: 16px;
+    bottom: 24px;
+    width: 100px;
+    height: 100px;
+    border-radius: 8px;
+  }
+}
+</style>

+ 4 - 35
src/components/open-app/open-app.vue

@@ -3,9 +3,9 @@
     <view class="open_App">
     <view class="open_App">
       <view style="display: flex; align-items: center;">
       <view style="display: flex; align-items: center;">
         <img src="@/static/logo.png" />
         <img src="@/static/logo.png" />
-        <text style="font-weight: 600;">OhPlay猫王妙播</text>
+        <text style="font-weight: 600;">OhPlay猫王妙播Pro</text>
       </view>
       </view>
-      <uni-button type="submit" @click="open">打开App</uni-button>
+      <uni-button type="submit" @click="getOpen()">打开App</uni-button>
     </view>
     </view>
     <view style="height: 88rpx" v-if="isShow" />
     <view style="height: 88rpx" v-if="isShow" />
   </view>
   </view>
@@ -14,47 +14,16 @@
 <script>
 <script>
 export default {
 export default {
   props: {
   props: {
-    // 界面类型
-    page: {
-      type: Number,
-      default: null
-    },
-    // 频道类型
-    channel: {
-      type: Number,
-      default: null
-    },
-    // 音频类型
-    audioType: {
-      type: Number,
-      default: null
-    },
-    // 音频Id
-    audioId: {
-      type: Number,
-      default: null
-    },
     // 是否覆盖
     // 是否覆盖
     isShow: {
     isShow: {
       type: Boolean,
       type: Boolean,
       default: true
       default: true
     }
     }
   },
   },
-  watch: {
-    channel(val) {
-      return val
-    },
-    audioType(val) {
-      return val
-    },
-    audioId(val) {
-      return val
-    }
-  },
   methods: {
   methods: {
     // 打开app
     // 打开app
-    open() {
-      this.openApp(this.page, this.channel, this.audioType, this.audioId)
+    getOpen() {
+      this.$emit('open')
     }
     }
   }
   }
 }
 }

+ 1 - 1
src/components/uni-popup/uni-popup.vue

@@ -341,7 +341,7 @@
 			 */
 			 */
 			bottom(type) {
 			bottom(type) {
 				this.popupstyle = 'bottom'
 				this.popupstyle = 'bottom'
-				this.ani = ['slide-bottom']
+				this.ani = ['fade', 'slide-bottom']
 				this.transClass = {
 				this.transClass = {
 					position: 'fixed',
 					position: 'fixed',
 					left: 0,
 					left: 0,

+ 0 - 148
src/components/uni-segmented-control/uni-segmented-control.vue

@@ -1,148 +0,0 @@
-<template>
-  <view :class="[styleType === 'text'?'segmented-control--text' : 'segmented-control--button' ]"
-    :style="{ borderColor: styleType === 'text' ? '' : activeColor }" class="segmented-control">
-    <view v-for="(item, index) in values" :class="[ styleType === 'text' ? '': 'segmented-control__item--button',
-		index === currentIndex&&styleType === 'button' ? 'segmented-control__item--button--active': '',
-		index === 0&&styleType === 'button' ? 'segmented-control__item--button--first': '',
-			index === values.length - 1&&styleType === 'button' ? 'segmented-control__item--button--last': '' ]"
-      :key="index"
-      :style="{ backgroundColor: index === currentIndex && styleType === 'button' ? activeColor : '',borderColor: index === currentIndex&&styleType === 'text'||styleType === 'button'?activeColor:'transparent' }"
-      class="segmented-control__item" @click="_onClick(index)">
-      <view>
-        <text :style="{color:
-				    index === currentIndex
-				      ? styleType === 'text'
-				        ? activeColor
-				        : '#fff'
-				      : styleType === 'text'
-				        ? '#FFFFFFB3'
-				        : activeColor}" class="segmented-control__text"
-          :class="styleType === 'text' && index === currentIndex ? 'segmented-control__item--text': ''">{{ item }}</text>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script>
-/**
- * SegmentedControl 分段器
- * @description 用作不同视图的显示
- * @tutorial https://ext.dcloud.net.cn/plugin?id=54
- * @property {Number} current 当前选中的tab索引值,从0计数
- * @property {String} styleType = [button|text] 分段器样式类型
- * 	@value button 按钮类型
- * 	@value text 文字类型
- * @property {String} activeColor 选中的标签背景色与边框颜色
- * @property {Array} values 选项数组
- * @event {Function} clickItem 组件触发点击事件时触发,e={currentIndex}
- */
-
-export default {
-  name: 'UniSegmentedControl',
-  emits: ['clickItem'],
-  props: {
-    current: {
-      type: Number,
-      default: 0
-    },
-    values: {
-      type: Array,
-      default() {
-        return []
-      }
-    },
-    activeColor: {
-      type: String,
-      default: '#2979FF'
-    },
-    styleType: {
-      type: String,
-      default: 'button'
-    }
-  },
-  data() {
-    return {
-      currentIndex: 0
-    }
-  },
-  watch: {
-    current(val) {
-      if (val !== this.currentIndex) {
-        this.currentIndex = val
-      }
-    }
-  },
-  created() {
-    this.currentIndex = this.current
-  },
-  methods: {
-    _onClick(index) {
-      if (this.currentIndex !== index) {
-        this.currentIndex = index
-        this.$emit('clickItem', {
-          currentIndex: index
-        })
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.segmented-control {
-  /* #ifndef APP-NVUE */
-  display: flex;
-  box-sizing: border-box;
-  /* #endif */
-  flex-direction: row;
-  height: 36px;
-  overflow: hidden;
-  /* #ifdef H5 */
-  cursor: pointer;
-  /* #endif */
-}
-
-.segmented-control__item {
-  /* #ifndef APP-NVUE */
-  display: inline-flex;
-  box-sizing: border-box;
-  /* #endif */
-  position: relative;
-  flex: 1;
-  justify-content: center;
-  align-items: center;
-}
-
-.segmented-control__item--button {
-  border-style: solid;
-  border-top-width: 1px;
-  border-bottom-width: 1px;
-  border-right-width: 1px;
-  border-left-width: 0;
-}
-
-.segmented-control__item--button--first {
-  border-left-width: 1px;
-  border-top-left-radius: 5px;
-  border-bottom-left-radius: 5px;
-}
-
-.segmented-control__item--button--last {
-  border-top-right-radius: 5px;
-  border-bottom-right-radius: 5px;
-}
-
-.segmented-control__item--text {
-  border-bottom-style: solid;
-  border-bottom-width: 2px;
-  font-size: 36rpx !important;
-  font-weight: bold;
-  padding: 6px 0;
-}
-
-.segmented-control__text {
-  font-size: 32rpx;
-  line-height: 20px;
-  text-align: center;
-}
-</style>

+ 576 - 0
src/components/uv-parse/node/node.vue

@@ -0,0 +1,576 @@
+<template>
+  <view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
+    <block v-for="(n, i) in childs" v-bind:key="i">
+      <!-- 图片 -->
+      <!-- 占位图 -->
+      <image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
+      <!-- 显示图片 -->
+      <!-- #ifdef H5 || (APP-PLUS && VUE2) -->
+      <img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || (APP-PLUS && VUE2) -->
+      <!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
+      <rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style,src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || APP-PLUS -->
+      <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':'')" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- #ifdef APP-PLUS && VUE3 -->
+      <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- 文本 -->
+      <!-- #ifdef MP-WEIXIN -->
+      <text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
+      <!-- #endif -->
+      <!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
+      <text v-else-if="n.text" decode>{{n.text}}</text>
+      <!-- #endif -->
+      <text v-else-if="n.name==='br'">\n</text>
+      <!-- 链接 -->
+      <view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
+        <node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
+      </view>
+      <!-- 视频 -->
+      <!-- #ifdef APP-PLUS -->
+      <view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" @vplay.stop="play" />
+      <!-- #endif -->
+      <!-- #ifndef APP-PLUS -->
+      <video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
+      <!-- #endif -->
+      <!-- #ifdef H5 || APP-PLUS -->
+      <iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
+      <embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
+      <!-- #endif -->
+      <!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
+      <!-- 音频 -->
+      <audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
+      <!-- #endif -->
+      <view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
+        <node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
+        <view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
+          <node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
+          <block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
+            <view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
+              <node :childs="tr.children" :opts="opts" />
+            </view>
+            <view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
+              <view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
+                <node :childs="td.children" :opts="opts" />
+              </view>
+            </view>
+          </block>
+        </view>
+      </view>
+      
+      <!-- 富文本 -->
+      <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
+      <rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
+      <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
+      <!-- #endif -->
+      <!-- 继续递归 -->
+      <view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
+        <node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
+      </view>
+      <node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
+    </block>
+  </view>
+</template>
+<script module="handler" lang="wxs">
+// 行内标签列表
+var inlineTags = {
+  abbr: true,
+  b: true,
+  big: true,
+  code: true,
+  del: true,
+  em: true,
+  i: true,
+  ins: true,
+  label: true,
+  q: true,
+  small: true,
+  span: true,
+  strong: true,
+  sub: true,
+  sup: true
+}
+/**
+ * @description 判断是否为行内标签
+ */
+module.exports = {
+  isInline: function (tagName, style) {
+    return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
+  }
+}
+</script>
+<script>
+
+import node from './node'
+export default {
+  name: 'node',
+  options: {
+    // #ifdef MP-WEIXIN
+    virtualHost: true,
+    // #endif
+    // #ifdef MP-TOUTIAO
+    addGlobalClass: false
+    // #endif
+  },
+  data () {
+    return {
+      ctrl: {},
+      // #ifdef MP-WEIXIN
+      isiOS: uni.getSystemInfoSync().system.includes('iOS')
+      // #endif
+    }
+  },
+  props: {
+    name: String,
+    attrs: {
+      type: Object,
+      default () {
+        return {}
+      }
+    },
+    childs: Array,
+    opts: Array
+  },
+  components: {
+
+    // #ifndef (H5 || APP-PLUS) && VUE3
+    node
+    // #endif
+  },
+  mounted () {
+    this.$nextTick(() => {
+      for (this.root = this.$parent; this.root.$options.name !== 'uv-parse'; this.root = this.root.$parent);
+    })
+    // #ifdef H5 || APP-PLUS
+    if (this.opts[0]) {
+      let i
+      for (i = this.childs.length; i--;) {
+        if (this.childs[i].name === 'img') break
+      }
+      if (i !== -1) {
+        this.observer = uni.createIntersectionObserver(this).relativeToViewport({
+          top: 500,
+          bottom: 500
+        })
+        this.observer.observe('._img', res => {
+          if (res.intersectionRatio) {
+            this.$set(this.ctrl, 'load', 1)
+            this.observer.disconnect()
+          }
+        })
+      }
+    }
+    // #endif
+  },
+  beforeDestroy () {
+    // #ifdef H5 || APP-PLUS
+    if (this.observer) {
+      this.observer.disconnect()
+    }
+    // #endif
+  },
+  methods:{
+    // #ifdef MP-WEIXIN
+    toJSON () { return this },
+    // #endif
+    /**
+     * @description 播放视频事件
+     * @param {Event} e
+     */
+    play (e) {
+      this.root.$emit('play')
+      // #ifndef APP-PLUS
+      if (this.root.pauseVideo) {
+        let flag = false
+        const id = e.target.id
+        for (let i = this.root._videos.length; i--;) {
+          if (this.root._videos[i].id === id) {
+            flag = true
+          } else {
+            this.root._videos[i].pause() // 自动暂停其他视频
+          }
+        }
+        // 将自己加入列表
+        if (!flag) {
+          const ctx = uni.createVideoContext(id
+            // #ifndef MP-BAIDU
+            , this
+            // #endif
+          )
+          ctx.id = id
+          if (this.root.playbackRate) {
+            ctx.playbackRate(this.root.playbackRate)
+          }
+          this.root._videos.push(ctx)
+        }
+      }
+      // #endif
+    },
+
+    /**
+     * @description 图片点击事件
+     * @param {Event} e
+     */
+    imgTap (e) {
+      const node = this.childs[e.currentTarget.dataset.i]
+      if (node.a) {
+        this.linkTap(node.a)
+        return
+      }
+      if (node.attrs.ignore) return
+      // #ifdef H5 || APP-PLUS
+      node.attrs.src = node.attrs.src || node.attrs['data-src']
+      // #endif
+      this.root.$emit('imgtap', node.attrs)
+      // 自动预览图片
+      if (this.root.previewImg) {
+        uni.previewImage({
+          // #ifdef MP-WEIXIN
+          showmenu: this.root.showImgMenu,
+          // #endif
+          // #ifdef MP-ALIPAY
+          enablesavephoto: this.root.showImgMenu,
+          enableShowPhotoDownload: this.root.showImgMenu,
+          // #endif
+          current: parseInt(node.attrs.i),
+          urls: this.root.imgList
+        })
+      }
+    },
+
+    /**
+     * @description 图片长按
+     */
+    imgLongTap (e) {
+      // #ifdef APP-PLUS
+      const attrs = this.childs[e.currentTarget.dataset.i].attrs
+      if (this.opts[3] && !attrs.ignore) {
+        uni.showActionSheet({
+          itemList: ['保存图片'],
+          success: () => {
+            const save = path => {
+              uni.saveImageToPhotosAlbum({
+                filePath: path,
+                success () {
+                  uni.showToast({
+                    title: '保存成功'
+                  })
+                }
+              })
+            }
+            if (this.root.imgList[attrs.i].startsWith('http')) {
+              uni.downloadFile({
+                url: this.root.imgList[attrs.i],
+                success: res => save(res.tempFilePath)
+              })
+            } else {
+              save(this.root.imgList[attrs.i])
+            }
+          }
+        })
+      }
+      // #endif
+    },
+
+    /**
+     * @description 图片加载完成事件
+     * @param {Event} e
+     */
+    imgLoad (e) {
+      const i = e.currentTarget.dataset.i
+      /* #ifndef H5 || (APP-PLUS && VUE2) */
+      if (!this.childs[i].w) {
+        // 设置原宽度
+        this.$set(this.ctrl, i, e.detail.width)
+      } else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
+        // 加载完毕,取消加载中占位图
+        this.$set(this.ctrl, i, 1)
+      }
+      this.checkReady()
+    },
+
+    /**
+     * @description 检查是否所有图片加载完毕
+     */
+    checkReady () {
+      if (this.root && !this.root.lazyLoad) {
+        this.root._unloadimgs -= 1
+        if (!this.root._unloadimgs) {
+          setTimeout(() => {
+            this.root.getRect().then(rect => {
+              this.root.$emit('ready', rect)
+            }).catch(() => {
+              this.root.$emit('ready', {})
+            })
+          }, 350)
+        }
+      }
+    },
+
+    /**
+     * @description 链接点击事件
+     * @param {Event} e
+     */
+    linkTap (e) {
+      const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
+      const attrs = node.attrs || e
+      const href = attrs.href
+      this.root.$emit('linktap', Object.assign({
+        innerText: this.root.getText(node.children || []) // 链接内的文本内容
+      }, attrs))
+      if (href) {
+        if (href[0] === '#') {
+          // 跳转锚点
+          this.root.navigateTo(href.substring(1)).catch(() => { })
+        } else if (href.split('?')[0].includes('://')) {
+          // 复制外部链接
+          if (this.root.copyLink) {
+            // #ifdef H5
+            window.open(href)
+            // #endif
+            // #ifdef MP
+            uni.setClipboardData({
+              data: href,
+              success: () =>
+                uni.showToast({
+                  title: '链接已复制'
+                })
+            })
+            // #endif
+            // #ifdef APP-PLUS
+            plus.runtime.openWeb(href)
+            // #endif
+          }
+        } else {
+          // 跳转页面
+          uni.navigateTo({
+            url: href,
+            fail () {
+              uni.switchTab({
+                url: href,
+                fail () { }
+              })
+            }
+          })
+        }
+      }
+    },
+
+    /**
+     * @description 错误事件
+     * @param {Event} e
+     */
+    mediaError (e) {
+      const i = e.currentTarget.dataset.i
+      const node = this.childs[i]
+      // 加载其他源
+      if (node.name === 'video' || node.name === 'audio') {
+        let index = (this.ctrl[i] || 0) + 1
+        if (index > node.src.length) {
+          index = 0
+        }
+        if (index < node.src.length) {
+          this.$set(this.ctrl, i, index)
+          return
+        }
+      } else if (node.name === 'img') {
+        // #ifdef H5 && VUE3
+        if (this.opts[0] && !this.ctrl.load) return
+        // #endif
+        // 显示错误占位图
+        if (this.opts[2]) {
+          this.$set(this.ctrl, i, -1)
+        }
+        this.checkReady()
+      }
+      if (this.root) {
+        this.root.$emit('error', {
+          source: node.name,
+          attrs: node.attrs,
+          // #ifndef H5 && VUE3
+          errMsg: e.detail.errMsg
+          // #endif
+        })
+      }
+    }
+  }
+}
+</script>
+<style>
+/* a 标签默认效果 */
+._a {
+  padding: 1.5px 0 1.5px 0;
+  color: deepskyblue;
+  word-break: break-all;
+}
+
+/* a 标签点击态效果 */
+._hover {
+  text-decoration: underline;
+  opacity: 0.7;
+}
+
+/* 图片默认效果 */
+._img {
+  max-width: 100%;
+  -webkit-touch-callout: none;
+}
+
+/* 内部样式 */
+
+._block {
+  display: block;
+}
+
+._b,
+._strong {
+  font-weight: bold;
+}
+
+._code {
+  font-family: monospace;
+}
+
+._del {
+  text-decoration: line-through;
+}
+
+._em,
+._i {
+  font-style: italic;
+}
+
+._h1 {
+  font-size: 2em;
+}
+
+._h2 {
+  font-size: 1.5em;
+}
+
+._h3 {
+  font-size: 1.17em;
+}
+
+._h5 {
+  font-size: 0.83em;
+}
+
+._h6 {
+  font-size: 0.67em;
+}
+
+._h1,
+._h2,
+._h3,
+._h4,
+._h5,
+._h6 {
+  display: block;
+  font-weight: bold;
+}
+
+._image {
+  height: 1px;
+}
+
+._ins {
+  text-decoration: underline;
+}
+
+._li {
+  display: list-item;
+}
+
+._ol {
+  list-style-type: decimal;
+}
+
+._ol,
+._ul {
+  display: block;
+  padding-left: 40px;
+  margin: 1em 0;
+}
+
+._q::before {
+  content: '"';
+}
+
+._q::after {
+  content: '"';
+}
+
+._sub {
+  font-size: smaller;
+  vertical-align: sub;
+}
+
+._sup {
+  font-size: smaller;
+  vertical-align: super;
+}
+
+._thead,
+._tbody,
+._tfoot {
+  display: table-row-group;
+}
+
+._tr {
+  display: table-row;
+}
+
+._td,
+._th {
+  display: table-cell;
+  vertical-align: middle;
+}
+
+._th {
+  font-weight: bold;
+  text-align: center;
+}
+
+._ul {
+  list-style-type: disc;
+}
+
+._ul ._ul {
+  margin: 0;
+  list-style-type: circle;
+}
+
+._ul ._ul ._ul {
+  list-style-type: square;
+}
+
+._abbr,
+._b,
+._code,
+._del,
+._em,
+._i,
+._ins,
+._label,
+._q,
+._span,
+._strong,
+._sub,
+._sup {
+  display: inline;
+}
+
+/* #ifdef APP-PLUS */
+._video {
+  width: 300px;
+  height: 225px;
+}
+/* #endif */
+</style>

Різницю між файлами не показано, бо вона завелика
+ 1335 - 0
src/components/uv-parse/parser.js


+ 498 - 0
src/components/uv-parse/uv-parse.vue

@@ -0,0 +1,498 @@
+<template>
+  <view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
+    <slot v-if="!nodes[0]" />
+    <!-- #ifndef APP-PLUS-NVUE -->
+    <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
+    <!-- #endif -->
+    <!-- #ifdef APP-PLUS-NVUE -->
+    <web-view ref="web" src="/uni_modules/uv-parse/static/app-plus/uv-parse/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+/**
+ * uv-parse v1.0.3
+ * @description 富文本组件
+ * @tutorial https://www.uvui.cn/components/parse.html
+ * @property {String} container-style 容器的样式
+ * @property {String} content 用于渲染的 html 字符串
+ * @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
+ * @property {String} domain 主域名,用于拼接链接
+ * @property {String} error-img 图片出错时的占位图链接
+ * @property {Boolean} lazy-load 是否开启图片懒加载
+ * @property {string} loading-img 图片加载过程中的占位图链接
+ * @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
+ * @property {Boolean} preview-img 是否允许图片被点击时自动预览
+ * @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
+ * @property {Boolean | String} selectable 是否开启长按复制
+ * @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
+ * @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
+ * @property {Object} tag-style 标签的默认样式
+ * @property {Boolean | Number} use-anchor 是否使用锚点链接
+ * @event {Function} load dom 结构加载完毕时触发
+ * @event {Function} ready 所有图片加载完毕时触发
+ * @event {Function} imgtap 图片被点击时触发
+ * @event {Function} linktap 链接被点击时触发
+ * @event {Function} play 音视频播放时触发
+ * @event {Function} error 媒体加载出错时触发
+ */
+// #ifndef APP-PLUS-NVUE
+import node from './node/node'
+// #endif
+import Parser from './parser'
+const plugins=[]
+// #ifdef APP-PLUS-NVUE
+const dom = weex.requireModule('dom')
+// #endif
+export default {
+  name: 'uv-parse',
+  data () {
+    return {
+      nodes: [],
+      // #ifdef APP-PLUS-NVUE
+      height: 3
+      // #endif
+    }
+  },
+  props: {
+    containerStyle: {
+      type: String,
+      default: ''
+    },
+    content: {
+      type: String,
+      default: ''
+    },
+    copyLink: {
+      type: [Boolean, String],
+      default: true
+    },
+    domain: String,
+    errorImg: {
+      type: String,
+      default: ''
+    },
+    lazyLoad: {
+      type: [Boolean, String],
+      default: false
+    },
+    loadingImg: {
+      type: String,
+      default: ''
+    },
+    pauseVideo: {
+      type: [Boolean, String],
+      default: true
+    },
+    previewImg: {
+      type: [Boolean, String],
+      default: true
+    },
+    scrollTable: [Boolean, String],
+    selectable: [Boolean, String],
+    setTitle: {
+      type: [Boolean, String],
+      default: true
+    },
+    showImgMenu: {
+      type: [Boolean, String],
+      default: true
+    },
+    tagStyle: Object,
+    useAnchor: [Boolean, Number]
+  },
+  // #ifdef VUE3
+  emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
+  // #endif
+  // #ifndef APP-PLUS-NVUE
+  components: {
+    node
+  },
+  // #endif
+  watch: {
+    content (content) {
+      this.setContent(content)
+    }
+  },
+  created () {
+    this.plugins = []
+    for (let i = plugins.length; i--;) {
+      this.plugins.push(new plugins[i](this))
+    }
+  },
+  mounted () {
+    if (this.content && !this.nodes.length) {
+      this.setContent(this.content)
+    }
+  },
+  beforeDestroy () {
+    this._hook('onDetached')
+  },
+  methods: {
+    /**
+     * @description 将锚点跳转的范围限定在一个 scroll-view 内
+     * @param {Object} page scroll-view 所在页面的示例
+     * @param {String} selector scroll-view 的选择器
+     * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
+     */
+    in (page, selector, scrollTop) {
+      // #ifndef APP-PLUS-NVUE
+      if (page && selector && scrollTop) {
+        this._in = {
+          page,
+          selector,
+          scrollTop
+        }
+      }
+      // #endif
+    },
+
+    /**
+     * @description 锚点跳转
+     * @param {String} id 要跳转的锚点 id
+     * @param {Number} offset 跳转位置的偏移量
+     * @returns {Promise}
+     */
+    navigateTo (id, offset) {
+      return new Promise((resolve, reject) => {
+        if (!this.useAnchor) {
+          reject(Error('Anchor is disabled'))
+          return
+        }
+        offset = offset || parseInt(this.useAnchor) || 0
+        // #ifdef APP-PLUS-NVUE
+        if (!id) {
+          dom.scrollToElement(this.$refs.web, {
+            offset
+          })
+          resolve()
+        } else {
+          this._navigateTo = {
+            resolve,
+            reject,
+            offset
+          }
+          this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
+        }
+        // #endif
+        // #ifndef APP-PLUS-NVUE
+        let deep = ' '
+        // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
+        deep = '>>>'
+        // #endif
+        const selector = uni.createSelectorQuery()
+          // #ifndef MP-ALIPAY
+          .in(this._in ? this._in.page : this)
+          // #endif
+          .select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
+        if (this._in) {
+          selector.select(this._in.selector).scrollOffset()
+            .select(this._in.selector).boundingClientRect()
+        } else {
+          // 获取 scroll-view 的位置和滚动距离
+          selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
+        }
+        selector.exec(res => {
+          if (!res[0]) {
+            reject(Error('Label not found'))
+            return
+          }
+          const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
+          if (this._in) {
+            // scroll-view 跳转
+            this._in.page[this._in.scrollTop] = scrollTop
+          } else {
+            // 页面跳转
+            uni.pageScrollTo({
+              scrollTop,
+              duration: 300
+            })
+          }
+          resolve()
+        })
+        // #endif
+      })
+    },
+
+    /**
+     * @description 获取文本内容
+     * @return {String}
+     */
+    getText (nodes) {
+      let text = '';
+      (function traversal (nodes) {
+        for (let i = 0; i < nodes.length; i++) {
+          const node = nodes[i]
+          if (node.type === 'text') {
+            text += node.text.replace(/&amp;/g, '&')
+          } else if (node.name === 'br') {
+            text += '\n'
+          } else {
+            // 块级标签前后加换行
+            const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
+            if (isBlock && text && text[text.length - 1] !== '\n') {
+              text += '\n'
+            }
+            // 递归获取子节点的文本
+            if (node.children) {
+              traversal(node.children)
+            }
+            if (isBlock && text[text.length - 1] !== '\n') {
+              text += '\n'
+            } else if (node.name === 'td' || node.name === 'th') {
+              text += '\t'
+            }
+          }
+        }
+      })(nodes || this.nodes)
+      return text
+    },
+
+    /**
+     * @description 获取内容大小和位置
+     * @return {Promise}
+     */
+    getRect () {
+      return new Promise((resolve, reject) => {
+        uni.createSelectorQuery()
+          // #ifndef MP-ALIPAY
+          .in(this)
+          // #endif
+          .select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
+      })
+    },
+
+    /**
+     * @description 暂停播放媒体
+     */
+    pauseMedia () {
+      for (let i = (this._videos || []).length; i--;) {
+        this._videos[i].pause()
+      }
+      // #ifdef APP-PLUS
+      const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
+      // #ifndef APP-PLUS-NVUE
+      let page = this.$parent
+      while (!page.$scope) page = page.$parent
+      page.$scope.$getAppWebview().evalJS(command)
+      // #endif
+      // #ifdef APP-PLUS-NVUE
+      this.$refs.web.evalJs(command)
+      // #endif
+      // #endif
+    },
+
+    /**
+     * @description 设置媒体播放速率
+     * @param {Number} rate 播放速率
+     */
+    setPlaybackRate (rate) {
+      this.playbackRate = rate
+      for (let i = (this._videos || []).length; i--;) {
+        this._videos[i].playbackRate(rate)
+      }
+      // #ifdef APP-PLUS
+      const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
+      // #ifndef APP-PLUS-NVUE
+      let page = this.$parent
+      while (!page.$scope) page = page.$parent
+      page.$scope.$getAppWebview().evalJS(command)
+      // #endif
+      // #ifdef APP-PLUS-NVUE
+      this.$refs.web.evalJs(command)
+      // #endif
+      // #endif
+    },
+
+    /**
+     * @description 设置内容
+     * @param {String} content html 内容
+     * @param {Boolean} append 是否在尾部追加
+     */
+    setContent (content, append) {
+      if (!append || !this.imgList) {
+        this.imgList = []
+      }
+      const nodes = new Parser(this).parse(content)
+      // #ifdef APP-PLUS-NVUE
+      if (this._ready) {
+        this._set(nodes, append)
+      }
+      // #endif
+      this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
+
+      // #ifndef APP-PLUS-NVUE
+      this._videos = []
+      this.$nextTick(() => {
+        this._hook('onLoad')
+        this.$emit('load')
+      })
+
+      if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
+        // 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
+        let height = 0
+        const callback = rect => {
+          if (!rect || !rect.height) rect = {}
+          // 350ms 总高度无变化就触发 ready 事件
+          if (rect.height === height) {
+            this.$emit('ready', rect)
+          } else {
+            height = rect.height
+            setTimeout(() => {
+              this.getRect().then(callback).catch(callback)
+            }, 350)
+          }
+        }
+        this.getRect().then(callback).catch(callback)
+      } else {
+        // 未设置懒加载,等待所有图片加载完毕
+        if (!this.imgList._unloadimgs) {
+          this.getRect().then(rect => {
+            this.$emit('ready', rect)
+          }).catch(() => {
+            this.$emit('ready', {})
+          })
+        }
+      }
+      // #endif
+    },
+
+    /**
+     * @description 调用插件钩子函数
+     */
+    _hook (name) {
+      for (let i = plugins.length; i--;) {
+        if (this.plugins[i][name]) {
+          this.plugins[i][name]()
+        }
+      }
+    },
+
+    // #ifdef APP-PLUS-NVUE
+    /**
+     * @description 设置内容
+     */
+    _set (nodes, append) {
+      this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
+    },
+
+    /**
+     * @description 接收到 web-view 消息
+     */
+    _onMessage (e) {
+      const message = e.detail.data[0]
+      switch (message.action) {
+        // web-view 初始化完毕
+        case 'onJSBridgeReady':
+          this._ready = true
+          if (this.nodes) {
+            this._set(this.nodes)
+          }
+          break
+        // 内容 dom 加载完毕
+        case 'onLoad':
+          this.height = message.height
+          this._hook('onLoad')
+          this.$emit('load')
+          break
+        // 所有图片加载完毕
+        case 'onReady':
+          this.getRect().then(res => {
+            this.$emit('ready', res)
+          }).catch(() => {
+            this.$emit('ready', {})
+          })
+          break
+        // 总高度发生变化
+        case 'onHeightChange':
+          this.height = message.height
+          break
+        // 图片点击
+        case 'onImgTap':
+          this.$emit('imgtap', message.attrs)
+          if (this.previewImg) {
+            uni.previewImage({
+              current: parseInt(message.attrs.i),
+              urls: this.imgList
+            })
+          }
+          break
+        // 链接点击
+        case 'onLinkTap': {
+          const href = message.attrs.href
+          this.$emit('linktap', message.attrs)
+          if (href) {
+            // 锚点跳转
+            if (href[0] === '#') {
+              if (this.useAnchor) {
+                dom.scrollToElement(this.$refs.web, {
+                  offset: message.offset
+                })
+              }
+            } else if (href.includes('://')) {
+              // 打开外链
+              if (this.copyLink) {
+                plus.runtime.openWeb(href)
+              }
+            } else {
+              uni.navigateTo({
+                url: href,
+                fail () {
+                  uni.switchTab({
+                    url: href
+                  })
+                }
+              })
+            }
+          }
+          break
+        }
+        case 'onPlay':
+          this.$emit('play')
+          break
+        // 获取到锚点的偏移量
+        case 'getOffset':
+          if (typeof message.offset === 'number') {
+            dom.scrollToElement(this.$refs.web, {
+              offset: message.offset + this._navigateTo.offset
+            })
+            this._navigateTo.resolve()
+          } else {
+            this._navigateTo.reject(Error('Label not found'))
+          }
+          break
+        // 点击
+        case 'onClick':
+          this.$emit('tap')
+          this.$emit('click')
+          break
+        // 出错
+        case 'onError':
+          this.$emit('error', {
+            source: message.source,
+            attrs: message.attrs
+          })
+      }
+    }
+    // #endif
+  }
+}
+</script>
+
+<style>
+/* #ifndef APP-PLUS-NVUE */
+/* 根节点样式 */
+._root {
+  /* padding: 1px 0; */
+  overflow-x: auto;
+  overflow-y: hidden;
+  -webkit-overflow-scrolling: touch;
+}
+
+/* 长按复制 */
+._select {
+  user-select: text;
+}
+/* #endif */
+</style>

+ 10 - 36
src/main.js

@@ -1,46 +1,20 @@
 import Vue from 'vue'
 import Vue from 'vue'
 import App from './App'
 import App from './App'
+import axios from 'axios'
+import openApp from './utils/openApp'
+import VConsole from 'vconsole'
+import { parseTime } from './utils/parseTime'
 
 
 Vue.config.productionTip = false
 Vue.config.productionTip = false
 
 
-// 打开App
-Vue.prototype.openApp = function (page, channel, audioType, audioId) {
-  // 
-  const res = uni.getSystemInfoSync()
-  var ua = window.navigator.userAgent.toLowerCase();
-  if (ua.match(/MicroMessenger/i) == 'micromessenger' || ua.match(/WeiBo/i) == "weibo" || ua.match(/QQ/i) == "qq") {
-    uni.showToast({
-      icon: 'none',
-      title: '右上角在浏览器打开',
-      mask: true
-    })
-  } else {
-    if (res.platform === 'ios') {
-      window.location.href = `airsmart://?page=${page}&channel=${channel}&audioType=${audioType}&audioId=${audioId}`
-    } else {
-      window.location.href = `airsmart://com.muzen.radioplayer:8888/main?page=${page}&channel=${channel}&audioType=${audioType}&audioId=${audioId}`
-    }
-
-    var timer = setTimeout(() => {
-      if (res.platform === 'ios') {
-        window.location.href = 'itms-appss://itunes.apple.com/cn/app/apple-store/id1621419943?mt=8'
-      } else {
-        window.location.href = 'https://music-play.oss-cn-shenzhen.aliyuncs.com/backOss/file/bacde3d529014ad08fcddb6baaffbc28.apk'
-      }
-    }, 3000);
-  }
+Vue.prototype.$axios = axios
 
 
-  document.addEventListener('visibilitychange', function () {
-    if (document.hidden) {
-      clearTimeout(timer)
-    }
-  })
+// 打开App
+Vue.prototype.openApp = openApp
+Vue.prototype.parseTime = parseTime
 
 
-  window.onpagehide = event => {
-    if (event.persisted) {
-      clearTimeout(timer)
-    }
-  }
+if (process.env.VUE_APP_ENV !== 'production') {
+  Vue.prototype.vconsole = new VConsole()
 }
 }
 
 
 App.mpType = 'app'
 App.mpType = 'app'

+ 83 - 86
src/manifest.json

@@ -1,88 +1,85 @@
 {
 {
-	"name": "",
-	"appid": "",
-	"description": "",
-	"versionName": "1.0.0",
-	"versionCode": "100",
-	"transformPx": false,
-	"app-plus": {
-		/* 5+App特有相关 */
-		"usingComponents": true,
-		"splashscreen": {
-			"alwaysShowBeforeRender": true,
-			"waiting": true,
-			"autoclose": true,
-			"delay": 0
-		},
-		"modules": {
-			/* 模块配置 */
-
-		},
-		"distribute": {
-			/* 应用发布信息 */
-			"android": {
-				/* android打包配置 */
-				"permissions": ["<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
-					"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
-					"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
-					"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
-					"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
-					"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
-					"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
-					"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
-					"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
-					"<uses-permission android:name=\"android.permission.CAMERA\"/>",
-					"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
-					"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
-					"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
-					"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
-					"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
-					"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
-					"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
-					"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
-					"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
-					"<uses-feature android:name=\"android.hardware.camera\"/>",
-					"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
-					"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
-				]
-			},
-			"ios": {
-				/* ios打包配置 */
-
-			},
-			"sdkConfigs": {
-				/* SDK配置 */
-
-			}
-		}
-	},
-	"quickapp": {
-		/* 快应用特有相关 */
-
-	},
-	"mp-weixin": {
-		/* 微信小程序特有相关 */
-		"appid": "",
-		"setting": {
-			"urlCheck": false
-		},
-		"usingComponents": true
-	},
-	"mp-alipay": {
-		"usingComponents": true
-	},
-	"mp-baidu": {
-		"usingComponents": true
-	},
-	"mp-toutiao": {
-		"usingComponents": true
-	},
-	"mp-qq": {
-		"usingComponents": true
-	},
-	"h5": {
-		"router": {
-			"mode": "history"
-		}
-	}
+    "name": "",
+    "appid": "",
+    "description": "",
+    "versionName": "1.0.0",
+    "versionCode": "100",
+    "transformPx": false,
+    "app-plus": {
+        "webView": {},
+        /* 5+App特有相关 */
+        "usingComponents": true,
+        "splashscreen": {
+            "alwaysShowBeforeRender": true,
+            "waiting": true,
+            "autoclose": true,
+            "delay": 0
+        },
+        "modules": {},
+        /* 模块配置 */
+        "distribute": {
+            /* 应用发布信息 */
+            "android": {
+                /* android打包配置 */
+                "permissions": [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            "ios": {
+                /* ios打包配置 */
+            },
+            "sdkConfigs": {
+                /* SDK配置 */
+            }
+        }
+    },
+    /* SDK配置 */
+    "quickapp": {},
+    /* 快应用特有相关 */
+    "mp-weixin": {
+        /* 微信小程序特有相关 */
+        "appid": "",
+        "setting": {
+            "urlCheck": false
+        },
+        "usingComponents": true
+    },
+    "mp-alipay": {
+        "usingComponents": true
+    },
+    "mp-baidu": {
+        "usingComponents": true
+    },
+    "mp-toutiao": {
+        "usingComponents": true
+    },
+    "mp-qq": {
+        "usingComponents": true
+    },
+    "h5": {
+        "router": {
+            "mode": "history"
+        }
+    }
 }
 }

+ 217 - 109
src/pages.json

@@ -1,111 +1,219 @@
 {
 {
-	"easycom": {
-		"^z-(.*)": "@zebra-ui/swiper/components/z-$1/z-$1.vue"
-	},
-	"pages": [{
-		// 设备引导页
-		"path": "pages/guide/index",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 设备文章
-		"path": "pages/article/index",
-		"style": {
-			"navigationStyle": "custom",
-			"onReachBottomDistance": 50
-		}
-	}, {
-		// 耳机操作手势
-		"path": "pages/operation/index",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 帮助与反馈
-		"path": "pages/help/index",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 产品手册
-		"path": "pages/product/manual",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 关于我们
-		"path": "pages/about/index",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 用户协议
-		"path": "pages/user/agreement",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 隐私协议
-		"path": "pages/privacy/agreement",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 猫王博物馆
-		"path": "pages/museum/index",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 猫王博物馆 设备详情页
-		"path": "pages/museum/detail",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 12频道分享
-		"path": "pages/share/channels",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 频道详情
-		"path": "pages/share/list",
-		"style": {
-			"navigationStyle": "custom",
-			"onReachBottomDistance": 50
-		}
-	}, {
-		// 歌单
-		"path": "pages/share/detail",
-		"style": {
-			"navigationStyle": "custom",
-			"onReachBottomDistance": 50
-		}
-	}, {
-		// 播放控件
-		"path": "pages/share/controls",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// vip音乐服务
-		"path": "pages/vip/index",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 抽奖
-		"path": "pages/lottery/index",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}, {
-		// 填写收货信息
-		"path": "pages/lottery/detail",
-		"style": {
-			"navigationStyle": "custom"
-		}
-	}]
+  "easycom": {
+    "^z-(.*)": "@zebra-ui/swiper/components/z-$1/z-$1.vue"
+  },
+  "pages": [
+    {
+      // 设备引导页
+      "path": "pages/guide/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      //设备操控视频页
+      "path": "pages/devices/detail",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 设备文章
+      "path": "pages/article/index",
+      "style": {
+        "navigationStyle": "custom",
+        "onReachBottomDistance": 50
+      }
+    },
+    {
+      // 耳机操作手势
+      "path": "pages/operation/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 帮助与反馈
+      "path": "pages/help/index",
+      "style": {
+        "navigationStyle": "custom",
+        "onReachBottomDistance": 50
+      }
+    },
+    {
+      // 我的反馈
+      "path": "pages/help/detail",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 产品手册
+      "path": "pages/product/manual",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 猫王博物馆
+      "path": "pages/museum/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 猫王博物馆 设备详情页
+      "path": "pages/museum/detail",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 12频道分享
+      "path": "pages/share/channels",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 频道详情
+      "path": "pages/share/list",
+      "style": {
+        "navigationStyle": "custom",
+        "onReachBottomDistance": 50
+      }
+    },
+    {
+      // 歌单
+      "path": "pages/share/detail",
+      "style": {
+        "navigationStyle": "custom",
+        "onReachBottomDistance": 50
+      }
+    },
+    {
+      // 播放控件
+      "path": "pages/share/controls",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 抽奖
+      "path": "pages/lottery/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 填写收货信息
+      "path": "pages/lottery/detail",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 爱听支付
+      "path": "pages/imusic/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 后台运行保护协议
+      "path": "pages/backgrounder/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 活动页面
+      "path": "pages/activity/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 流量活动页面
+      "path": "pages/activity/new",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 协议页面
+      "path": "pages/agreement/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 文章页面
+      "path": "pages/content/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 门店列表
+      "path": "pages/map/index",
+      "style": {
+        "navigationBarTitleText": "附近门店",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 添加电台
+      "path": "pages/channel/index",
+      "style": {
+        "navigationStyle": "custom",
+        "onReachBottomDistance": 50
+      }
+    },
+    {
+      // 喜马拉雅授权
+      "path": "pages/ximalaya/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 喜马拉雅充值成功
+      "path": "pages/ximalaya/success",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    // 喜马拉雅充值失败
+    {
+      "path": "pages/ximalaya/error",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 流量充值
+      "path": "pages/pay/index",
+      "style": {
+        "navigationBarTitleText": "流量充值",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      // 开通记录
+      "path": "pages/pay/detail",
+      "style": {
+        "navigationBarTitleText": "开通记录",
+        "navigationStyle": "custom",
+        "onReachBottomDistance": 50
+      }
+    },
+    {
+      // 通用提示
+      "path": "pages/warning/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    }
+  ]
 }
 }

+ 140 - 0
src/pages/activity/index.vue

@@ -0,0 +1,140 @@
+<template>
+  <view v-if="pic" class="app-container">
+    <img :src="pic" />
+    <button class="submit" type="submit" circle @click="getSubmit">
+      立即兑换
+    </button>
+
+    <!-- 弹窗 -->
+    <uni-popup ref="popup" type="center" :is-mask-click="false">
+      <view class="popup">
+        <h3>兑换成功</h3>
+        <view class="center">{{ content }}</view>
+        <view class="submit-btn">
+          <button type="submit" circle @click="getClose">确定</button>
+          <span>该套餐内容仅可在WIFI/移动数据模式下收听</span>
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script>
+import { detail, submit } from "@/api/activity.js";
+export default {
+  data() {
+    return {
+      pic: "",
+      form: {
+        activityId: "",
+        clientType: getApp().globalData.userInfo.deviceClientType,
+        deviceMac: getApp().globalData.userInfo.deviceMac,
+        type: getApp().globalData.deviceInfo.deviceMode,
+      },
+      // 弹窗内容
+      content: "",
+      // 兑换成功
+      isShow: false,
+    };
+  },
+  onLoad(e) {
+    this.form.activityId = e.activityId;
+    this.getDetail();
+  },
+  methods: {
+    getDetail() {
+      detail({
+        activityId: this.form.activityId,
+      }).then((res) => {
+        console.log("gadfadsfqewrqerq====" + JSON.stringify(res));
+        if (res.code === 0) {
+          this.pic = res.data.pic;
+        } else {
+          uni.showToast({
+            title: res.data.message,
+            icon: "error",
+            duration: 2000,
+          });
+        }
+      });
+    },
+
+    // 立即领取
+    getSubmit() {
+      submit(this.form).then((res) => {
+        if (res.code === 0) {
+          this.$refs.popup.open();
+          this.content = res.data;
+          this.isShow = true;
+        } else {
+          uni.showToast({
+            title: res.data.message,
+            icon: "none",
+            mask: true,
+            duration: 2000,
+          });
+        }
+      });
+    },
+
+    // 关闭弹窗
+    getClose() {
+      this.$refs.popup.close();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 0;
+  overflow: hidden;
+  position: relative;
+}
+
+img {
+  width: 100%;
+  height: 100%;
+  display: block;
+}
+
+.submit {
+  position: absolute;
+  bottom: 20rpx;
+  left: 50%;
+  transform: translate(-50%);
+  width: calc(100% - 120rpx);
+}
+
+.popup {
+  width: 576rpx;
+  height: 600rpx;
+  background: #fff;
+  border-radius: 32rpx;
+  color: #000;
+  font-weight: bold;
+  text-align: center;
+  padding: 48rpx 0;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  align-items: center;
+
+  .center {
+    font-size: 32rpx;
+  }
+
+  .submit-btn {
+    button {
+      width: 320rpx;
+      height: 80rpx;
+      line-height: 80rpx;
+      font-size: 32rpx;
+    }
+
+    span {
+      font-size: 24rpx;
+    }
+  }
+}
+</style>

+ 127 - 0
src/pages/activity/new.vue

@@ -0,0 +1,127 @@
+<template>
+  <view v-if="pic" class="app-container">
+    <img :src="pic" />
+    <button class="submit" type="submit" circle @click="getSubmit">
+      立即领取
+    </button>
+
+    <!-- 弹窗 -->
+    <uni-popup ref="popup" type="center" :is-mask-click="false">
+      <view class="popup">
+        <h3>领取成功</h3>
+        <view class="center">{{ content }}</view>
+        <view class="submit-btn">
+          <button type="submit" circle @click="getClose">确定</button>
+          <!--          <span>该套餐内容仅可在WIFI/移动数据模式下收听</span>-->
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script>
+import { doNewActivity } from "@/api/activity.js";
+export default {
+  data() {
+    return {
+      pic: "",
+      form: {
+        activityId: "",
+        state: "",
+      },
+      // 弹窗内容
+      content: "",
+      // 领取成功
+      isShow: false,
+    };
+  },
+
+  onLoad(e) {
+    this.id = e.id;
+    this.name = e.name;
+    this.pic = e.pic;
+    this.description = e.description;
+    this.form.activityId = e.id;
+    this.form.state = e.state;
+  },
+
+  methods: {
+    // 立即领取
+    getSubmit() {
+      doNewActivity(this.form).then((res) => {
+        if (res.code === 0) {
+          this.$refs.popup.open();
+          this.content = res.data;
+          this.isShow = true;
+        } else {
+          uni.showToast({
+            title: res.message,
+            icon: "none",
+            mask: true,
+            duration: 2000,
+          });
+        }
+      });
+    },
+
+    // 关闭弹窗
+    getClose() {
+      this.$refs.popup.close();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 0;
+  overflow: hidden;
+  position: relative;
+}
+
+img {
+  width: 100%;
+  height: 100%;
+  display: block;
+}
+
+.submit {
+  position: absolute;
+  bottom: 20rpx;
+  left: 50%;
+  transform: translate(-50%);
+  width: calc(100% - 120rpx);
+}
+
+.popup {
+  width: 576rpx;
+  height: 600rpx;
+  background: #fff;
+  border-radius: 32rpx;
+  color: #000;
+  font-weight: bold;
+  text-align: center;
+  padding: 48rpx 0;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  align-items: center;
+
+  .center {
+    font-size: 32rpx;
+  }
+
+  .submit-btn {
+    button {
+      width: 320rpx;
+      height: 80rpx;
+      line-height: 80rpx;
+      font-size: 32rpx;
+    }
+
+    span {
+      font-size: 24rpx;
+    }
+  }
+}
+</style>

+ 41 - 0
src/pages/agreement/index.vue

@@ -0,0 +1,41 @@
+<template>
+  <view class='app-container'>
+    <uv-parse :content="form.content" selectable @linktap="linktap" />
+  </view>
+</template>
+
+<script>
+import { detail } from '@/api/agreement.js'
+export default {
+  data() {
+    return {
+      // 表单
+      form: {}
+    }
+  },
+  onLoad(e) {
+    this.getDetail(e.type)
+  },
+  methods: {
+    getDetail(type) {
+      detail({
+        type: type
+      }).then(res => {
+        if (res.code === 0) {
+          this.form = res.data
+        }
+      })
+    },
+    // 点击链接
+    linktap(e) {
+      window.location.href = e.href
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container{
+  white-space: pre-wrap;
+}
+</style>

+ 10 - 10
src/pages/article/index.vue

@@ -12,8 +12,8 @@
           <view flex center>
           <view flex center>
             <img class="avatar" :src="item.userAvatar" />
             <img class="avatar" :src="item.userAvatar" />
             <view style="opacity: 0.7; margin-left: 20rpx">
             <view style="opacity: 0.7; margin-left: 20rpx">
-              <view style="fontSize: 26rpx">{{ item.userName }}</view>
-              <view style="fontSize: 22rpx">{{ item.createTime }}</view>
+              <view style="font-size: 26rpx">{{ item.userName }}</view>
+              <view style="font-size: 22rpx">{{ item.createTime }}</view>
             </view>
             </view>
           </view>
           </view>
           <view flex center style="opacity: 0.7">
           <view flex center style="opacity: 0.7">
@@ -22,7 +22,7 @@
           </view>
           </view>
         </view>
         </view>
         <view class="item-content">
         <view class="item-content">
-          <view style="fontSize:28rpx">
+          <view style="font-size:28rpx">
             {{ item.content }}
             {{ item.content }}
           </view>
           </view>
           <view class="sub-content" v-if="item.replyList.length > 0">
           <view class="sub-content" v-if="item.replyList.length > 0">
@@ -78,10 +78,10 @@ export default {
       detail({
       detail({
         articleId: e.articleId
         articleId: e.articleId
       }).then(res => {
       }).then(res => {
-        if (res.data.code === 0) {
-          this.url = res.data.data.pic
-          this.title = res.data.data.title
-          this.content = res.data.data.content
+        if (res.code === 0) {
+          this.url = res.data.pic
+          this.title = res.data.title
+          this.content = res.data.content
         }
         }
       })
       })
       this.getList()
       this.getList()
@@ -105,11 +105,11 @@ export default {
   methods: {
   methods: {
     getList() {
     getList() {
       list(this.form).then(res => {
       list(this.form).then(res => {
-        if (res.data.code === 0) {
-          res.data.data.records.map(i => {
+        if (res.code === 0) {
+          res.data.records.map(i => {
             this.tableData.push(i)
             this.tableData.push(i)
           })
           })
-          this.hasMore = res.data.data.hasMore
+          this.hasMore = res.data.hasMore
         }
         }
       })
       })
     }
     }

+ 3 - 5
src/pages/about/index.vue

@@ -1,7 +1,5 @@
 <template>
 <template>
-  <view class="app-container">
-
-  </view>
+  <div class="app-container"></div>
 </template>
 </template>
 
 
 <script>
 <script>
@@ -12,11 +10,11 @@ export default {
     }
     }
   },
   },
   created(){
   created(){
-    window.location.href = "https://ohplay.radio1964.net/about"
+    window.location.href = 'https://ohplay.radio1964.net/guide'
   }
   }
 }
 }
 </script>
 </script>
 
 
 <style lang="scss">
 <style lang="scss">
 
 
-</style>
+</style>

+ 216 - 0
src/pages/channel/index.vue

@@ -0,0 +1,216 @@
+<template>
+  <div class='app-container'>
+    <h1>猫王音响-添加电台</h1>
+    <h1>请输入要添加的网络电台信息</h1>
+    <div class="form" v-for="(item, index) in form" :key="index">
+      <div>电台{{ index + 1 }}</div>
+      <uni-icons v-show="index !== 0" class="close" type="closeempty" color="#FFF" size="18" @click="getClose(index)" />
+      <input type="text" v-model="item.name" placeholder="请输入电台名称">
+      <input type="text" v-model="item.url" placeholder="请输入链接地址">
+    </div>
+    <button v-show="form.length < 3" class="plus" @click="getAdd">+ 新增</button>
+    <button class="submit" type="submit" circle @click="getSubmit">提交</button>
+    <div class="list">
+      <h2>已添加的电台</h2>
+      <div class="item" v-for="item in list" :key="item.id">
+        <div>{{ item.name }}</div>
+        <div class="url">{{ item.url }}</div>
+        <uni-icons class="delete" type="more-filled" size="24" @click="getDelete(item)" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { list, submit, remove } from "@/api/channel"
+export default {
+  data() {
+    return {
+      // 设备信息
+      dev: {},
+      // 列表表单
+      listForm: {
+        pageNum: 1,
+        pageSize: 10
+      },
+      // 列表
+      list: [],
+      // 更多
+      hasMore: false,
+      // 表单
+      form: [{
+        name: "",
+        url: ""
+      }]
+    }
+  },
+  onLoad(e) {
+    this.dev = e
+    this.getList()
+  },
+  methods: {
+    // 列表
+    getList() {
+      list({ ...this.dev, ...this.listForm }).then(res => {
+        if (res.code === 0) {
+          res.data.records.map(i => this.list.push(i))
+          this.hasMore = res.data.hasMore
+        }
+      })
+    },
+
+    // 添加
+    getAdd() {
+      this.form.push({
+        name: "",
+        url: ""
+      })
+    },
+
+    // 关闭
+    getClose(index) {
+      this.form.splice(index, 1)
+    },
+
+    // 提交
+    getSubmit() {
+      var rules = false
+      this.form.map(i => {
+        if (i.name !== "" && i.url !== "") {
+          i.deviceMac = this.dev.deviceMac
+          i.deviceType = this.dev.deviceType
+          i.sign = this.dev.sign
+          rules = true
+        }
+      })
+      if (rules) {
+        submit(this.form).then(res => {
+          if (res.code === 0) {
+            this.list = []
+            this.listForm.pageNum = 1
+            this.getList()
+            this.form = [{
+              name: "",
+              url: ""
+            }]
+          }
+        })
+      } else {
+        uni.showToast({
+          title: '电台信息不能为空!',
+          icon: 'none'
+        })
+      }
+    },
+
+    // 删除
+    getDelete(item) {
+      uni.showModal({
+        title: '提示',
+        content: `是否删除名称为${item.name}的电台?`,
+        confirmColor: '#78B06A',
+        success: (e) => {
+          if (e.confirm) {
+            remove({
+              id: item.id,
+              ...this.dev
+            }).then(res => {
+              if (res.code === 0) {
+                this.list = []
+                this.listForm.pageNum = 1
+                this.getList(this.dev)
+              }
+            })
+          }
+        }
+      })
+    }
+  },
+  onReachBottom() {
+    if(this.hasMore) {
+      this.listForm.pageNum++
+      this.getList()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.plus {
+  width: 160rpx;
+  height: 70rpx;
+  line-height: 70rpx;
+  font-size: 28rpx;
+  margin: 0;
+  font-weight: bold;
+  background: none;
+  color: #FFF;
+  border: 1px solid #FFF;
+}
+
+h1 {
+  font-size: 16px;
+  line-height: 50px;
+  text-align: center;
+}
+
+h2 {
+  font-size: 16px;
+  line-height: 50px;
+}
+
+.list {
+  .item {
+    border: 1px solid #FFF;
+    border-radius: 8px;
+    padding: 20px 30px 20px 20px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    position: relative;
+    margin-bottom: 15px;
+    background-color: #FFF;
+    color: #000;
+
+    .delete {
+      position: absolute;
+      right: 10px;
+      transform: rotate(90deg);
+    }
+
+    .url{
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+}
+
+.form {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  margin: 8px 0;
+  border: 1px solid #FFF;
+  padding: 20px;
+  border-radius: 8px;
+  position: relative;
+
+  uni-input {
+    background-color: #FFF;
+    color: #000;
+  }
+
+  .close {
+    position: absolute;
+    right: 10px;
+    top: 10px;
+  }
+}
+
+.submit {
+  height: 80rpx;
+  line-height: 80rpx;
+  margin-top: 10px;
+}
+</style>

+ 214 - 0
src/pages/content/index.vue

@@ -0,0 +1,214 @@
+<template>
+  <view class='app-container' :style="{ backgroundImage: `url(${form.backgroundImage})` }">
+    <view class="content" :style="{ 'padding-bottom': form.buttonFixType === 0 ? '94px' : 0 }">
+      <!-- 打开app -->
+      <open-app v-if="!inside" @open="getOpenApp" />
+      <!-- 页面布局 -->
+      <view v-if="form.contentShareLayoutList">
+        <view class="item" v-for="item in form.contentShareLayoutList" :key="item.id">
+          <!-- 图片 -->
+          <img v-if="item.layoutType === 0" :src="item.contentPic" />
+          <!-- 音频 -->
+          <my-audio v-if="item.layoutType === 1" :ref="`video${item.sort}`" :bg="form.audioBackgroundImage"
+            :pic="item.contentPic" :name="item.contentName" :singer="item.contentInfo.singerName"
+            :status="item.playStatus" @click="getPlay(item)" />
+          <!-- 歌单 、专辑 -->
+          <my-list v-if="item.layoutType === 2" :pic="item.contentPic" :name="item.contentName" :data="item.contentInfo"
+            :type="item.contentType" @click="getOpen(item)" />
+        </view>
+      </view>
+    </view>
+    <!-- 按钮 -->
+    <view :class="['footer', form.buttonFixType === 0 ? 'fixed' : '']"
+      v-if="inside ? form.isButton == 1 ? true : false : false">
+      <button class="left" @click="getClick(form.leftButtonForwardType, form, 'leftButton')"
+        :style="{ background: form.leftButtonImg ? `url(${form.leftButtonImg}) no-repeat 100% / 100%` : '' }">
+        <text>{{ form.leftButtonText }}</text>
+      </button>
+      <button class="right" @click="getClick(form.rightButtonForwardType, form, 'rightButton')"
+        :style="{ background: form.rightButtonImg ? `url(${form.rightButtonImg}) no-repeat 100% / 100%` : '' }">
+        <text>{{ form.rightButtonText }}</text>
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { detail } from '@/api/content'
+export default {
+  data() {
+    return {
+      // 判断是否为APP内部H5
+      inside: getApp().globalData.inside,
+      // 表单
+      form: {},
+      // 跳转地址
+      audioOption: {
+        8: '/mobile/playAlbumPage',
+        10: '/mobile/playSongsPage',
+        15: '/mobile/playSongsPage'
+      }
+    }
+  },
+  onLoad(e) {
+    if (e.articleId) {
+      this.getDetail(e.articleId)
+    }
+  },
+  methods: {
+    getDetail(articleId) {
+      detail({
+        articleId: articleId
+      }).then(res => {
+        if (res.code === 0) {
+          if (res.data.templateType == 1) {
+            // 按sort从小到大排序
+            res.data.contentShareLayoutList.sort(function (x, y) {
+              return x.sort > y.sort ? 1 : -1
+            })
+            // 给单音频添加播放状态
+            res.data.contentShareLayoutList.map(i => {
+              if ([2, 6, 11].includes(i.contentType)) {
+                i.playStatus = 2
+              }
+            })
+          }
+
+          this.form = res.data
+          // app端标题会省去特殊符号,做处理
+          this.form.title = encodeURIComponent(res.data.title)  
+
+          // 检测当前是否有正在播放的音频
+          if (this.inside) {
+            this.getStatus()
+          }
+        } else {
+          uni.showToast({
+            icon: 'error',
+            title: res.data.message
+          })
+        }
+      })
+    },
+
+    // 获取播放状态
+    getStatus() {
+      playStatus.postMessage(`获取当前播放状态`)
+      window['setOpenPage'] = res => {
+        let e = JSON.parse(res)
+        this.form.contentShareLayoutList.find(i => {
+          if (i.contentId == e.audioId) {
+            i.playStatus = e.status
+          }
+        })
+      }
+    },
+
+    // 单个音频
+    getPlay(item) {
+      if (this.inside) {
+        let e = item.playStatus !== 1 ? 1 : 2 
+        openPage.postMessage(`?page=/mobile/audioPage&audioId=${item.contentInfo.audioId}&audioType=${item.contentInfo.audioType}&platformId=${item.platformId}&playStatus=${e}&openAudioPage=0&cmd=openPage`)
+        window['setOpenPage'] = res => {
+          this.form.contentShareLayoutList.map(i => i.playStatus = 2)
+          item.playStatus = JSON.parse(res).status
+        }
+      } else {
+        this.getOpenApp()
+      }
+    },
+
+    // 歌单 、专辑
+    getOpen(item) {
+      if (this.inside) {
+        openPage.postMessage(`?page=${this.audioOption[item.contentType]}&audioId=${item.contentInfo.audioId}&audioType=${item.contentInfo.audioType}&platformId=${item.platformId}&openAudioPage=1&cmd=openPage`)
+      } else {
+        this.getOpenApp()
+      }
+    },
+
+    // 浏览器H5打开App内部H5
+    getOpenApp() {
+      this.openApp(`?page=/mobile/webViewPage&url=pages/content/index?articleId=${this.form.id}&cmd=openPage&isNeedLogin=1&title=${this.form.title}`)
+    },
+
+    // 底部按钮
+    getClick(type, form, key) {
+      if (type === 0) {
+        // H5内链
+        window.location.href = form[`${key}ForwardUrl`]
+      } else if (type === 1) {
+        // App跳转音频合集
+        openPage.postMessage(`?page=${this.audioOption[form[`${key}ContentType`]]}&audioId=${form[`${key}ContentId`]}&audioType=${form[`${key}ContentType`]}&playformId=${form[`${key}PlatformId`]}&openAudioPage=1&cmd=openPage`)
+      } else {
+        // 分享
+        openShare.postMessage(`?contentId=${form.id}&shareType=-4`)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  display: flex;
+  flex-flow: column nowrap;
+  word-break: break-all;
+  padding: 0;
+  font-weight: 100;
+  min-height: 100%;
+  background-size: 100% 100%;
+  background-repeat: no-repeat;
+
+  .content {
+    flex: 1;
+    padding: 24px 16px;
+
+    .title {
+      margin-bottom: 24px;
+      color: #FFF;
+      font-weight: bold;
+    }
+
+    .item {
+      margin-bottom: 24px;
+
+      img {
+        width: 100%;
+      }
+    }
+  }
+
+  .footer {
+    width: 100%;
+    height: 94px;
+    background: #414141;
+    display: flex;
+    justify-content: space-around;
+    padding-top: 24px;
+
+    button {
+      background: linear-gradient(180deg, #A4D099 0%, #78B06A 100%);
+      height: 46px;
+      line-height: 46px;
+      font-size: 16px;
+      border-radius: 25px;
+      color: #FFF;
+      font-weight: bold;
+    }
+
+    .left {
+      width: 220px;
+    }
+
+    .right {
+      width: 107px;
+    }
+  }
+}
+
+.fixed {
+  position: fixed;
+  bottom: 0;
+}
+</style>

+ 45 - 0
src/pages/devices/detail.vue

@@ -0,0 +1,45 @@
+<template>
+  <view class="detail">
+    <uv-parse :content="content" :preview-img="false" />
+  </view>
+</template>
+
+<script>
+import { detail } from '@/api/devices'
+export default {
+  data() {
+    return {
+      content: ''
+    }
+  },
+  onLoad(e) {
+    this.getDetail(e)
+  },
+  methods: {
+    getDetail(e) {
+      detail(e).then(res => {
+        if (res.code === 0) {
+          this.content = res.data.content
+        } else {
+          uni.showToast({
+            title: res.data.message,
+            icon: 'error',
+            mask: true,
+            duration: 2000
+          })
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.detail {
+  white-space: pre-wrap;
+}
+
+::v-deep img {
+  display: block;
+}
+</style>

+ 14 - 6
src/pages/guide/index.vue

@@ -11,7 +11,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { getContent } from '@/api/guide'
+import { detail } from '@/api/guide'
 export default {
 export default {
   data() {
   data() {
     return {
     return {
@@ -19,10 +19,14 @@ export default {
     }
     }
   },
   },
   onLoad(e) {
   onLoad(e) {
-    if (e.deviceModel) {
-      getContent({ clientType: e.deviceModel }).then(res => {
-        if (res.data.code === 0) {
-          this.content = res.data.data.guidePageContent
+    if (e) {
+      let data = {
+        clientType: e.deviceModel,
+        id: e.id
+      }
+      detail(data).then(res => {
+        if (res.code === 0) {
+          this.content = res.data.guidePageContent
         }
         }
       })
       })
     }
     }
@@ -44,7 +48,7 @@ export default {
     overflow-y: scroll;
     overflow-y: scroll;
   }
   }
 
 
-  .no-data{
+  .no-data {
     position: absolute;
     position: absolute;
     top: 50%;
     top: 50%;
     left: 50%;
     left: 50%;
@@ -59,4 +63,8 @@ export default {
     width: calc(100% - 64rpx);
     width: calc(100% - 64rpx);
   }
   }
 }
 }
+
+img {
+  width: 100%;
+}
 </style>
 </style>

+ 130 - 0
src/pages/help/detail.vue

@@ -0,0 +1,130 @@
+<template>
+  <view class='app-container'>
+    <view class="list">
+      <view class="item" v-for="item in tableData" :key="item.opinionId">
+        <view :class="['status', item.status === 1 ? 'status-info' : 'status-success']">
+          {{ status[item.status] }}
+        </view>
+        <view class="content">
+          {{ item.content }}
+        </view>
+        <view v-if="item.status === 3">
+          <span>回复:</span>
+          <view class="reply">{{ item.reply }}</view>
+        </view>
+      </view>
+    </view>
+    <view class="has-more" v-if="!hasMore">
+      —————— 暂无更多反馈 ——————
+    </view>
+  </view>
+</template>
+
+<script>
+import { detail } from '@/api/help.js'
+export default {
+  data() {
+    return {
+      // 表单
+      form: {
+        pageNum: 1,
+        pageSize: 10
+      },
+      // 是否有更多
+      hasMore: false,
+      // 列表
+      tableData: [],
+      // 状态
+      status: {
+        1: '待回复',
+        3: '已回复'
+      }
+    }
+  },
+  onLoad() {
+    this.getDetail()
+  },
+  methods: {
+    getDetail() {
+      detail(this.form).then(res => {
+        if (res.code === 0) {
+          res.data.records.map(i => {
+            this.tableData.push(i)
+          })
+          this.hasMore = res.data.hasMore
+        }
+      })
+    }
+  },
+  onReachBottom() {
+    if (this.hasMore) {
+      this.form.pageNum++
+      this.getDetail()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  background-color: #FAFAFA;
+  color: #353535;
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  word-wrap: break-word;
+
+  .list {
+    flex: 1;
+
+    .item {
+      background-color: #FFF;
+      padding: 0 32rpx 32rpx;
+      margin-bottom: 40rpx;
+      border-radius: 16rpx;
+
+      .status {
+        padding: 8rpx 20rpx;
+        display: inline-block;
+        border-bottom-left-radius: 16rpx;
+        border-bottom-right-radius: 16rpx;
+        font-size: 24rpx;
+        color: #FFF;
+        margin-bottom: 24rpx;
+      }
+
+      .status-info {
+        background-color: #999;
+      }
+
+      .status-success {
+        background: linear-gradient(180deg, #A4D099 0%, #78B06A 100%);
+      }
+
+      .content {
+        font-size: 28rpx;
+      }
+
+      span {
+        color: #999;
+        font-size: 24rpx;
+        margin: 16rpx 0;
+        display: block;
+      }
+
+      .reply {
+        background-color: #E0E0E0;
+        font-size: 24rpx;
+        padding: 20rpx;
+        border-radius: 16rpx;
+      }
+    }
+  }
+
+  .has-more {
+    color: #999;
+    font-size: 24rpx;
+    text-align: center;
+  }
+}
+</style>

+ 219 - 5
src/pages/help/index.vue

@@ -1,22 +1,236 @@
 <template>
 <template>
   <view class="app-container">
   <view class="app-container">
-
+    <view class="list">
+      <view class="item margin-bottom">
+        <h3>问题类型</h3>
+      </view>
+      <view class="item">
+        <radio-group @change="radioChange">
+          <label v-for="item in typeOptions" :key="item.id">
+            <radio :value="item.id.toString()" color="#A4D099" :checked="item.id == active"
+              style="transform: scale(0.7); margin: -7rpx;" />
+            <span>{{ item.name }}</span>
+          </label>
+        </radio-group>
+      </view>
+      <view class="item margin-bottom">
+        <textarea v-model="form.content" maxlength="200" placeholder="请描述您遇到的问题或意见" />
+      </view>
+      <view class="item upload margin-bottom">
+        <view class="img-list" v-for="(item, index) in imageFiles" :key="index">
+          <uni-icons class="close" type="closeempty" color="#FFF" size="12" @click="getDelete(index)" />
+          <img :src="item" />
+        </view>
+        <button v-if="imageFiles.length < 9" @click="upload">
+          <uni-icons type="plusempty" color="#A4D099" size="36" />
+        </button>
+      </view>
+    </view>
+    <view class="submit">
+      <button type="submit" circle @click="getSubmit">提交</button>
+      <view class="contact">
+        <span>客服微信:miao_friend</span>
+        <span>客服电话:4008508199</span>
+      </view>
+    </view>
   </view>
   </view>
 </template>
 </template>
 
 
 <script>
 <script>
+import { list, submit } from '@/api/help.js'
 export default {
 export default {
   data() {
   data() {
+    const info = getApp().globalData.userInfo
     return {
     return {
-
+      // 表单
+      form: {
+        appVersion: info.version,
+        phone: info.phone,
+        platformType: info.deviceType,
+        systemVersion: info.systemVersion,
+        imageFiles: '',
+        type: '',
+        content: ''
+      },
+      // 图片列表
+      imageFiles: [],
+      length: 0,
+      // 单选
+      active: 0,
+      // 反馈类型
+      typeOptions: []
     }
     }
   },
   },
-  created(){
-    window.location.href = 'https://ohplay.radio1964.net/help'
+  onLoad() {
+    this.getList()
+  },
+  methods: {
+    // 反馈类型
+    getList() {
+      list().then(res => {
+        if (res.code === 0) {
+          this.typeOptions = res.data
+        }
+      })
+    },
+
+    // 单选
+    radioChange(e) {
+      this.active = e.detail.value
+      this.form.type = e.detail.value
+    },
+
+    // 上传
+    upload() {
+      selectImageCount.postMessage(this.length)
+      selectImage.postMessage('获取相册信息')
+      window['receiveImageAddress'] = e => {
+        JSON.parse(e).addressList.map(i => {
+          this.imageFiles.push(i)
+        })
+        this.length = this.imageFiles.length
+      }
+    },
+
+    // 删除按钮
+    getDelete(index) {
+      this.imageFiles.splice(index, 1)
+      this.length = this.imageFiles.length
+    },
+
+    // 提交
+    getSubmit() {
+      if (this.form.type !== '' && this.form.content !== '') {
+        this.form.imageFiles = this.imageFiles.join(',')
+        submit(this.form).then(res => {
+          if (res.code === 0) {
+            uni.showToast({
+              title: '提交成功!'
+            })
+            this.form.imageFiles = ''
+            this.form.type = ''
+            this.active = 0
+            this.form.content = ''
+            this.imageFiles = []
+          } else {
+            uni.showToast({
+              title: res.data.message,
+              icon: 'error'
+            })
+          }
+        })
+      } else {
+        uni.showToast({
+          title: '请选择问题类型并且描述您遇到的问题或意见',
+          icon: 'none',
+          duration: 3000
+        })
+      }
+    }
   }
   }
 }
 }
 </script>
 </script>
 
 
-<style lang="scss">
+<style lang="scss" scoped>
+.app-container {
+  background-color: #FFF;
+  color: #353535;
+  display: flex;
+  flex-flow: column nowrap;
+  min-height: 100vh;
+
+  .list {
+    flex: 1;
+
+    .item {
+      uni-radio-group {
+        display: flex;
+        flex-wrap: wrap;
+
+        label {
+          width: calc(25% - 10px);
+          font-size: 24rpx;
+          display: flex;
+          align-items: flex-start;
+          margin: 0 10px 20px 0;
+        }
+
+        span {
+          word-break: break-all;
+        }
+      }
+
+      textarea {
+        border-radius: 8px;
+        height: 240rpx;
+        background-color: #FAFAFA;
+      }
+    }
+
+    .upload {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12px;
 
 
+      .img-list {
+        display: flex;
+        position: relative;
+
+        .close {
+          width: 40rpx;
+          height: 40rpx;
+          line-height: 40rpx;
+          border-radius: 50%;
+          position: absolute;
+          right: 0rpx;
+          top: 0rpx;
+          background-color: #888;
+        }
+
+        img {
+          width: 80px;
+          height: 80px;
+          border-radius: 16rpx;
+        }
+      }
+
+      button {
+        width: 80px;
+        height: 80px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin: 0;
+        background-color: #F0F7EE;
+        border-radius: 16rpx;
+      }
+    }
+  }
+
+  .submit {
+    width: 100%;
+    margin-top: 100px;
+
+    button {
+      width: 400rpx;
+      height: 80rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .contact {
+      width: 100%;
+      font-size: 24rpx;
+      color: #999;
+      display: flex;
+      justify-content: space-around;
+      margin-top: 95rpx;
+    }
+  }
+}
+
+.margin-bottom {
+  margin-bottom: 20px;
+}
 </style>
 </style>

+ 122 - 0
src/pages/imusic/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <view class="index">
+    <image mode="center" :src="form.pic" />
+    <view class="buy">
+      <button type="buy" circle @click="getSubmit">立即购买</button>
+      <span>
+        <radio-group @change="getChange">
+          <radio :value="value" />
+        </radio-group>
+        <text>购买即视为同意</text>
+        <navigator url="/pages/agreement/index?type=5">《场景歌单付费协议》</navigator>
+        <navigator url="/pages/agreement/index?type=2">《音乐服务许可协议》</navigator>
+      </span>
+    </view>
+  </view>
+</template>
+
+<script>
+import { detail } from '@/api/imusic.js'
+export default {
+  data() {
+    return {
+      form: {},
+      // 是否同意
+      value: '0',
+      id: ''
+    }
+  },
+  onLoad(e) {
+    if (e.id) {
+      this.getDetail(e.id)
+      this.id = e.id
+    }
+  },
+  methods: {
+    getDetail(e) {
+      detail({ id: e }).then(res => {
+        if (res.code === 0) {
+          this.form = res.data
+        }
+      })
+    },
+
+    getChange() {
+      this.value = '1'
+    },
+
+    // 立即购买
+    getSubmit() {
+      if (this.value === '1') {
+        clickPay.postMessage(`{\"id\":\"${this.id}\", \"type\":15}`)
+      } else {
+        uni.showToast({
+          title: '请先阅读并勾选协议',
+          icon: 'none'
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.index {
+  width: 100%;
+  height: 100%;
+  overflow-y: hidden;
+}
+
+uni-image {
+  width: 100%;
+  height: 100%;
+}
+
+.buy {
+  position: fixed;
+  bottom: 0;
+  width: 100%;
+  height: 232rpx;
+  overflow: hidden;
+}
+
+.buy::before {
+  content: '';
+  display: block;
+  height: 232rpx;
+  background: rgba(18, 18, 18, 0.4);
+  backdrop-filter: blur(16rpx);
+}
+
+button {
+  width: 92%;
+  font-weight: bold;
+  position: absolute;
+  top: 24rpx;
+  left: 50%;
+  transform: translate(-50%);
+}
+
+navigator {
+  display: inline;
+  color: #FFF;
+}
+
+span {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: absolute;
+  bottom: 60rpx;
+  width: 100%;
+  text-align: center;
+  font-size: 24rpx;
+  color: #d5d5d5;
+}
+
+::v-deep uni-radio .uni-radio-input {
+  width: 24rpx;
+  height: 24rpx;
+  background: none;
+}
+</style>

+ 2 - 2
src/pages/lottery/detail.vue

@@ -43,7 +43,7 @@ export default {
           rules: [{
           rules: [{
             required: true, errorMessage: '请输入手机号'
             required: true, errorMessage: '请输入手机号'
           }, {
           }, {
-            pattern: /^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\d{8}$/, errorMessage: '请输入正确的手机号'
+            pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, errorMessage: '请输入正确的手机号'
           }]
           }]
         },
         },
         receiveAddress: {
         receiveAddress: {
@@ -74,7 +74,7 @@ export default {
         if (!valid) {
         if (!valid) {
           this.disabled = true
           this.disabled = true
           receive(getApp().globalData.userInfo, this.form).then(res => {
           receive(getApp().globalData.userInfo, this.form).then(res => {
-            if (res.data.code === 0) {
+            if (res.code === 0) {
               uni.showToast({
               uni.showToast({
                 title: '提交成功!',
                 title: '提交成功!',
                 duration: 3000
                 duration: 3000

+ 9 - 9
src/pages/lottery/index.vue

@@ -27,14 +27,14 @@
     <!-- 弹窗 -->
     <!-- 弹窗 -->
     <uni-popup ref="popup" type="center" :is-mask-click="false">
     <uni-popup ref="popup" type="center" :is-mask-click="false">
       <view v-if="this.hasLotteryCount <= 0" class="popup" style="justify-content: space-around">
       <view v-if="this.hasLotteryCount <= 0" class="popup" style="justify-content: space-around">
-        <view style="fontSize: 36rpx">
+        <view style="font-size: 36rpx">
           <view>很遗憾!</view>
           <view>很遗憾!</view>
           <view>今日抽奖次数已用光</view>
           <view>今日抽奖次数已用光</view>
         </view>
         </view>
         <img src="../../static/lottery/noChance.png" />
         <img src="../../static/lottery/noChance.png" />
       </view>
       </view>
       <view v-else class="popup">
       <view v-else class="popup">
-        <view style="fontSize: 36rpx">{{ form.resultGoodName }}</view>
+        <view style="font-size: 36rpx">{{ form.resultGoodName }}</view>
         <img :src="form.resultGoodPic" />
         <img :src="form.resultGoodPic" />
         <button v-if="form.resultGoodType === 4" @click="getSubmit">领取</button>
         <button v-if="form.resultGoodType === 4" @click="getSubmit">领取</button>
         <button v-if="form.resultGoodType === 5" @click="getAgain">再抽一次</button>
         <button v-if="form.resultGoodType === 5" @click="getAgain">再抽一次</button>
@@ -87,15 +87,14 @@ export default {
   },
   },
   onLoad() {
   onLoad() {
     this.getList()
     this.getList()
-    console.log(getApp());
   },
   },
   methods: {
   methods: {
     // 获取页面信息
     // 获取页面信息
     getList() {
     getList() {
       this.prizes = []
       this.prizes = []
       page(getApp().globalData.userInfo).then(res => {
       page(getApp().globalData.userInfo).then(res => {
-        if (res.data.code === 0) {
-          const j = res.data.data
+        if (res.code === 0) {
+          const j = res.data
           // 可用积分
           // 可用积分
           this.maySignPoint = j.maySignPoint
           this.maySignPoint = j.maySignPoint
           // 每次消耗积分
           // 每次消耗积分
@@ -137,10 +136,10 @@ export default {
             this.$refs.lucky.play()
             this.$refs.lucky.play()
             this.disabled = false
             this.disabled = false
             result(getApp().globalData.userInfo).then(res => {
             result(getApp().globalData.userInfo).then(res => {
-              if (res.data.code === 0) {
+              if (res.code === 0) {
                 setTimeout(() => {
                 setTimeout(() => {
-                  this.$refs.lucky.stop(res.data.data.resultGoodSort - 1)
-                  this.form = res.data.data
+                  this.$refs.lucky.stop(res.data.resultGoodSort - 1)
+                  this.form = res.data
                 }, 3000)
                 }, 3000)
               } else {
               } else {
                 this.$refs.lucky.stop()
                 this.$refs.lucky.stop()
@@ -171,6 +170,7 @@ export default {
 
 
     // 再抽一次
     // 再抽一次
     getAgain() {
     getAgain() {
+      this.getList()
       this.$refs.popup.close()
       this.$refs.popup.close()
       this.startCallBack()
       this.startCallBack()
     },
     },
@@ -183,7 +183,7 @@ export default {
           prizeId: this.form.resultGoodId,
           prizeId: this.form.resultGoodId,
           lotteryCode: this.form.lotteryCode
           lotteryCode: this.form.lotteryCode
         }).then(res => {
         }).then(res => {
-          if (res.data.code === 0) {
+          if (res.code === 0) {
             this.$refs.popup.close()
             this.$refs.popup.close()
             this.getList()
             this.getList()
             setTimeout(() => {
             setTimeout(() => {

+ 568 - 0
src/pages/map/index.vue

@@ -0,0 +1,568 @@
+<template>
+  <view class='app-container'>
+    <!-- 定位 + 城市选择器 -->
+    <view class="tab" flex between>
+      <view class="address" @click="getRep">
+        <uni-icons :type="location === '定位中...' ? 'spinner-cycle' : 'location'" />
+        <text>{{ location }}</text>
+      </view>
+      <picker class="list" mode="multiSelector" :range="locationList" range-key="name" :value="pickerValue"
+        @columnchange="handleColumn" @change="handleChange">
+        <view flex style="width: 100%">
+          <view class="item" flex>
+            <view class="name">{{ province }}</view>
+            <uni-icons type="bottom" />
+          </view>
+          <view class="item" flex>
+            <view class="name">{{ city }}</view>
+            <uni-icons type="bottom" />
+          </view>
+          <view class="item" flex>
+            <view class="name">{{ district }}</view>
+            <uni-icons type="bottom" />
+          </view>
+        </view>
+      </picker>
+    </view>
+    <!-- 地图 -->
+    <view id="container" />
+    <!-- 范围内的门店 -->
+    <scroll-view scroll-y class="poi" @scrolltolower="scrollTolower">
+      <view v-if="list.length > 0" style="height: 100%; padding: 16px">
+        <view :class="['list', active === index ? 'active' : '']" flex center v-for="(item, index) in list" :key="item.id"
+          @click="changeActive(item, index)">
+          <img :src="item.icon" width="80" height="80" />
+          <view class="info">
+            <view class="name">{{ item.name }}</view>
+            <view class="distance">距离:{{ item.distance / 1000 }}KM</view>
+            <view class="address">地址:{{ item.address }}</view>
+          </view>
+          <view class="button" flex around>
+            <img class="icon" v-show="item.linkPhone" src="../../static/phone.png" @click="getPhone(item.linkPhone)" />
+            <img class="icon" src="../../static/address.png" @click="getMap(item)" />
+          </view>
+        </view>
+      </view>
+      <view v-else style="height: 100%;  padding: 0 16px">
+        <view class="storeList" v-for="item in storeList" :key="item.id" flex between>
+          <img :src="item.icon" width="56" height="56" />
+          <view class="info">
+            <view class="name">{{ item.name }}</view>
+          </view>
+          <button type="submit" @click="getStore(item.url)">进店逛逛</button>
+        </view>
+      </view>
+    </scroll-view>
+    <!-- 打开地图app弹窗 -->
+    <uni-popup ref="popup" type="bottom">
+      <view class="popup">
+        <view class="title" flex center>请选择地图</view>
+        <view class="item" flex center v-for="item in mapList" :key="item.value" @click="openMap(item)">
+          {{ item.name }}
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script>
+import AMapLoader from '@amap/amap-jsapi-loader'
+// 安全密钥
+window._AMapSecurityConfig = {
+  securityJsCode: 'fd78b73eba80ab3f887c9ba60744048f'
+}
+import { options, list, storeList } from '@/api/map'
+export default {
+  data() {
+    return {
+      // 是否为APP内部H5
+      inside: getApp().globalData.inside,
+      // 定位
+      location: '定位中...',
+      // 省市区列表
+      locationList: [],
+      // 选中焦点 
+      pickerValue: [18, 0, 0],
+      province: '',
+      city: '',
+      district: '',
+      // 表单
+      form: {
+        pageNum: 1,
+        pageSize: 10,
+        longitude: 116.397428,
+        latitude: 39.90923
+      },
+      // 线下店铺
+      list: [],
+      // 默认选中第一家店铺
+      active: 0,
+      // 第三方店铺
+      storeList: [],
+      // 店铺坐标
+      toLng: '',
+      toLat: '',
+      toName: '',
+
+      // 地图
+      AMap: null,
+      map: null,
+      geolocation: null,
+      // 地图App
+      mapList: [{
+        id: 1,
+        name: '腾讯地图',
+        value: 'qqMap'
+      }, {
+        id: 2,
+        name: '百度地图',
+        value: 'baiduMap'
+      }, {
+        id: 3,
+        name: '高德地图',
+        value: 'aMap'
+      }],
+      // 只触发一次
+      only: true
+    }
+  },
+  onLoad() {
+    this.initAMap()
+  },
+  methods: {
+    async initAMap() {
+      // 调用高德
+      this.AMap = await this.getMapLoader()
+      // 自定义定位按钮
+      this.geolocation = new AMap.Geolocation({
+        timeout: 3000, // 定位超时时间
+        showCircle: false, // 定位成功有个圆形范围
+        showButton: false // 是否显示按钮
+      })
+      const locationRes = await this.getLocation(this.geolocation)
+      // 当前定位
+      if (locationRes.status === 'complete') {
+        this.form.longitude = locationRes.data.lng
+        this.form.latitude = locationRes.data.lat
+      }
+      // 获取城市信息
+      this.getGeocoder([this.form.longitude, this.form.latitude], locationRes.status)
+      // 绘制地图
+      this.map = new AMap.Map('container', {
+        zoom: 16,
+        center: [this.form.longitude, this.form.latitude]
+      })
+      // 调用插件
+      this.map.addControl(this.geolocation)
+      // 地图加载完
+      this.map.on('complete', () => {
+        // 自动定位一次
+        this.geolocation.getCurrentPosition()
+      })
+    },
+
+    // 调用高德
+    getMapLoader() {
+      return new Promise((resolve, reject) => {
+        AMapLoader.load({
+          key: '5dcf4cf0260059dade51d0d9b5e3c1ef',
+          version: '2.0',
+          plugins: ['AMap.Geolocation', 'AMap.Geocoder']
+        }).then((AMap) => {
+          resolve(AMap)
+        }).catch((err) => {
+          reject(err)
+        })
+      })
+    },
+
+    // 获取定位
+    getLocation(geolocation) {
+      return new Promise((resolve, reject) => {
+        this.AMap.plugin('AMap.Geolocation', function () {
+          geolocation.getCurrentPosition(function (status, res) {
+            resolve({
+              status: status,
+              data: res.position
+            })
+          })
+        })
+      })
+    },
+
+    // 获取城市信息
+    async getGeocoder(lnglat, locationStatus) {
+      const that = this
+      this.AMap.plugin('AMap.Geocoder', function () {
+        var geocoder = new AMap.Geocoder()
+        geocoder.getAddress(lnglat, function (status, resolve) {
+          if (status === 'complete' && resolve.info === 'OK' && resolve.regeocode.formattedAddress !== '中华人民共和国') {
+            let e = resolve.regeocode.addressComponent
+            that.location = locationStatus === 'complete' ? `${e.city ? e.city : e.province}${e.district}` : that.location = '重新定位'
+            that.form.city = null
+            that.city = '全部'
+            that.form.area = null
+            that.district = '全部'
+            that.locationList = []
+            options().then(res => {
+              if (res.code === 0) {
+                let value = []
+                // 市
+                that.locationList[0] = res.data
+                value[0] = that.locationList[0].findIndex(i => e.province.includes(i.name))
+                that.form.province = that.locationList[0][value[0]].id
+                that.province = that.locationList[0][value[0]].name
+                that.locationList[1] = that.locationList[0][value[0]].childList
+                value[1] = that.locationList[1].findIndex(i => i.citycode === e.citycode)
+                // 省
+                if (value[1] !== -1) {
+                  that.form.city = that.locationList[1][value[1]].id
+                  that.city = that.locationList[1][value[1]].name
+                  // 区
+                  that.locationList[2] = that.locationList[1][value[1]].childList
+                  value[2] = that.locationList[2].findIndex(i => i.adcode === e.adcode)
+                  if (value[2] !== -1) {
+                    that.form.area = that.locationList[2][value[2]].id
+                    that.district = that.locationList[2][value[2]].name
+                  }
+                }
+                that.pickerValue = value
+                that.getList()
+              }
+            })
+          }
+        })
+      })
+    },
+
+    // 添加标点
+    getMarker(lng, lat) {
+      const icon = new AMap.Icon({
+        size: new AMap.Size(30, 30),
+        imageSize: new AMap.Size(30, 30),
+        image: '../../static/location.png'
+      })
+      const marker = new AMap.Marker({
+        position: new AMap.LngLat(lng, lat),
+        offset: new AMap.Pixel(-20, -25),
+        icon: icon
+      })
+      this.map.add(marker)
+    },
+
+    // 打开地图
+    openMap(e) {
+      this.$refs.popup.close()
+      let url = ''
+      if (this.inside) {
+        openOtherApp.postMessage(`?page=${e.value}&longitude=${this.toLng}&latitude=${this.toLat}&name=${this.toName}`)
+      } else {
+        // 腾讯地图
+        if (e.id === 1) {
+          url = `https://apis.map.qq.com/uri/v1/routeplan?type=drive&from=我的位置&fromcoord=${this.form.latitude},${this.form.longitude}&to=${this.toName}&tocoord=${this.toLat},${this.toLng}&coord_type=2&referer=猫王妙播Pro`
+        }
+
+        // 百度地图
+        if (e.id === 2) {
+          url = `http://api.map.baidu.com/direction?origin=latlng:${this.form.latitude},${this.form.longitude}|name:我的位置&destination=latlng:${this.toLat},${this.toLng}|name:${this.toName}&mode=driving&region=${this.province}&output=html&coord_type=gcj02&src=webapp.airsmart.猫王妙播Pro`
+        }
+
+        // 高德地图
+        if (e.id === 3) {
+          url = `https://uri.amap.com/navigation?from=${this.form.longitude},${this.form.latitude},我的位置&to=${this.toLng},${this.toLat},${this.toName}&callnative=1`
+        }
+        window.location.href = url
+      }
+    },
+
+    // 重新定位
+    getRep() {
+      this.location = '定位中...'
+      this.initAMap()
+    },
+
+    // 选择省
+    handleColumn(e) {
+      let column = e.detail.column
+      let index = e.detail.value
+      if (column === 0) {
+        this.pickerValue = [index, 0, 0]
+        this.locationList[1] = this.locationList[0][index].childList
+        this.locationList[2] = this.locationList[1].length > 0 ? this.locationList[1][0].childList : []
+      }
+      if (column === 1) {
+        this.pickerValue = [this.pickerValue[0], index, 0]
+        this.locationList[2] = this.locationList[1][index].childList
+      }
+    },
+
+    // 确定
+    handleChange(e) {
+      this.form.province = ''
+      this.form.city = ''
+      this.city = '全部'
+      this.form.area = ''
+      this.district = '全部'
+      let index = e.detail.value
+      this.form.province = this.locationList[0][index[0]].id
+      this.province = this.locationList[0][index[0]].name
+      if (this.locationList[1].length > 0) {
+        this.form.city = this.locationList[1][index[1]].id
+        this.city = this.locationList[1][index[1]].name
+      }
+      if (this.locationList[2].length > 0) {
+        this.form.area = this.locationList[2][index[2]].id
+        this.district = this.locationList[2][index[2]].name
+      }
+      this.getList()
+    },
+
+    // 线下店铺
+    getList() {
+      this.list = []
+      this.storeList = []
+      this.map.clearMap()
+      list(this.form).then(res => {
+        if (res.code === 0) {
+          if (res.data.records.length > 0) {
+            res.data.records.map(i => {
+              this.list.push(i)
+              this.getMarker(i.longitude, i.latitude)
+            })
+            this.hasMore = res.data.hasMore
+            this.map.setCenter([this.list[0].longitude, this.list[0].latitude])
+          } else {
+            this.getStoreList()
+          }
+        }
+      })
+    },
+
+    // 切换店铺
+    changeActive(item, index) {
+      this.active = index
+      this.map.setZoom(16)
+      this.map.setCenter([item.longitude, item.latitude])
+    },
+
+
+    // 第三方店铺
+    getStoreList() {
+      storeList().then(res => {
+        if (res.code === 0) {
+          this.storeList = res.data
+        }
+      })
+    },
+
+    // 跳转第三方店铺
+    getStore(url) {
+      if (this.inside) {
+        openOtherApp.postMessage(`?page=openOtherWeb&url=${url}`)
+      } else {
+        window.location.href = url
+      }
+    },
+
+    // 拨打电话
+    getPhone(e) {
+      if (this.inside) {
+        openOtherApp.postMessage(`?page=callPhone&name=${e}`)
+      } else {
+        uni.makePhoneCall({
+          phoneNumber: e
+        })
+      }
+    },
+
+    // 打开地图
+    getMap(e) {
+      this.$refs.popup.open()
+      // 店铺坐标
+      this.toLng = e.longitude
+      this.toLat = e.latitude
+      this.toName = e.name
+    },
+
+    // 触底
+    scrollTolower() {
+      if (this.hasMore) {
+        this.form.pageNum++
+        this.getList()
+      } else {
+        if (this.only) {
+          uni.showToast({
+            icon: 'none',
+            title: '暂无更多店铺'
+          })
+          this.only = false
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+  position: relative;
+  color: #000;
+  font-size: 28rpx;
+  overflow-y: hidden;
+  display: flex;
+  flex-direction: column;
+
+  .tab {
+    width: 100%;
+    height: 88rpx;
+    line-height: 88rpx;
+    padding: 0 12px;
+    background-color: #FFF;
+    white-space: nowrap;
+
+    .address {
+      width: 100px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+
+    uni-picker {
+      width: calc(100% - 100px);
+
+      .item {
+        width: calc(100% / 3);
+        margin: 0 5px;
+
+        .name {
+          width: 100%;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          text-align: right;
+        }
+      }
+    }
+  }
+
+  #container {
+    width: 100%;
+    height: 100%;
+    flex: 1;
+  }
+
+  .poi {
+    width: 100%;
+    height: 270px;
+    overflow-y: auto;
+    z-index: 99;
+    background-color: #FFF;
+
+    .list,
+    .storeList {
+      width: 100%;
+      height: 104px;
+      padding: 12px;
+      border-bottom: 1px solid #e8e8e8;
+
+      img {
+        border-radius: 8px;
+      }
+
+      button {
+        height: auto;
+        line-height: 1;
+        font-size: 12px;
+        padding: 8px 10px;
+        border-radius: 8px;
+        margin: 0;
+      }
+
+      .info {
+        flex: 1;
+        padding: 0 8px;
+        overflow: hidden;
+
+        .name {
+          font-weight: bold;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .distance,
+        .address {
+          font-size: 10px;
+          color: #999;
+        }
+      }
+
+      .button {
+        flex-direction: column;
+
+        .icon:first-child {
+          margin-bottom: 20px;
+        }
+
+        .icon {
+          width: 24px;
+          background-color: #a4d099;
+          border-radius: 50%;
+        }
+      }
+    }
+
+    .list {
+      border-radius: 16px;
+      margin-bottom: 12px;
+    }
+
+    .storeList:last-child {
+      border: none;
+    }
+
+    .active {
+      background-color: #F2F5F7;
+    }
+  }
+
+  .uni-icons {
+    vertical-align: bottom;
+  }
+}
+
+.uni-popup {
+  z-index: 999;
+
+  .popup {
+    width: 90%;
+    background-color: #FFF;
+    border-radius: 16px;
+    margin: 0 auto;
+    position: absolute;
+    bottom: 20px;
+    left: 50%;
+    transform: translate(-50%);
+
+    .title {
+      height: 40px;
+      font-size: 12px;
+      border-bottom: 1px solid #e8e8e8;
+    }
+
+    .item {
+      width: 100%;
+      height: 60px;
+      border-bottom: 1px solid #e8e8e8;
+      font-size: 18px;
+    }
+
+    .item:last-child {
+      border: none;
+    }
+  }
+}
+</style>

+ 1 - 1
src/pages/museum/img.json

@@ -94,7 +94,7 @@
     "url": "../../static/museum/20.png"
     "url": "../../static/museum/20.png"
   }, {
   }, {
     "id": 21,
     "id": 21,
-    "name": "贴贴音箱",
+    "name": "贴贴音箱小王子Meet",
     "url": "../../static/museum/21.png"
     "url": "../../static/museum/21.png"
   }]
   }]
 }]
 }]

+ 97 - 0
src/pages/pay/detail.vue

@@ -0,0 +1,97 @@
+<template>
+  <view class='app-container'>
+    <view class="list" v-for="item in tableData" :key="item.orderId">
+      <view class="name" flex between>
+        <view>
+          <text>{{ item.name }}-流量包</text>
+          <text :class="['flow-status', item.flowStatus === 0 ? 'used' : item.flowStatus === 1 ? 'unused' : 'over']">
+            {{ options[item.flowStatus] }}
+          </text>
+        </view>
+        <view>¥{{ item.goodDiscountedPrice }}</view>
+      </view>
+      <view class="time" flex between>
+        <view>购买时间:{{ parseTime(item.orderTimeStr, '{y}-{m}-{d}') }}</view>
+        <view>到期时间:{{ parseTime(item.expirationTime, '{y}-{m}-{d}') }}</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { list } from "@/api/pay.js";
+export default {
+  data() {
+    return {
+      form: {
+        pageNum: 1,
+        pageSize: 10,
+      },
+      tableData: [],
+      options: {
+        0: '已生效',
+        1: '待生效',
+        2: '已失效'
+      }
+    }
+  },
+  onLoad(e) {
+    this.form.state = e.state
+    this.getList()
+  },
+  methods: {
+    getList() {
+      list(this.form).then(res => {
+        if (res.code === 0) {
+          this.tableData=this.tableData.concat(res.data.records); 
+          this.hasMore = res.data.hasMore
+        }
+      })
+    }
+  },
+  onReachBottom() {
+    if (this.hasMore) {
+      this.form.pageNum++
+      this.getList()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scope>
+.list {
+  padding: 32rpx;
+  background: rgba(255, 255, 255, 0.05);
+  margin-bottom: 12rpx;
+  border-radius: 16rpx;
+}
+
+.name {
+  font-size: 28rpx;
+  margin-bottom: 12rpx;
+}
+
+.time {
+  font-size: 22rpx;
+  opacity: 0.5;
+}
+
+.flow-status {
+  font-size: 20rpx;
+  border-radius: 16rpx;
+  padding: 0 12rpx;
+  margin-left: 10rpx;
+}
+
+.used {
+  background: linear-gradient(180deg, #A4D099 0%, #78B06A 100%);
+}
+
+.unused {
+  background: rgba(153, 153, 153, 1);
+}
+
+.over {
+  background: rgba(83, 83, 83, 1);
+}
+</style>

+ 560 - 0
src/pages/pay/index.vue

@@ -0,0 +1,560 @@
+<template>
+  <view class="app-container" flex column>
+    <view :class="['header', orderList.length > 0 ? 'header-bg' : '']">
+      <view class="info" flex>
+        <view class="dev-img" flex center>
+          <img :src="info.devicePic" />
+        </view>
+        <view class="dev" flex column>
+          <view class="dev-name owt">{{ info.deviceName }}</view>
+          <view class="dev-history" @click="getRouter"
+            >开通记录
+            <uni-icons type="right" color="#9fa5ad" size="11" />
+          </view>
+        </view>
+      </view>
+      <view class="progress" flex column center>
+        <progress
+          :percent="percent"
+          activeColor="#FFD5A0"
+          backgroundColor="#746C64"
+          border-radius="20"
+        />
+        <view class="progress-info" flex between>
+          <view>
+            <text
+              >剩余:{{ (this.rateplan.left / 1024).toFixed(2) }}GB / 总量:{{
+                (this.rateplan.total / 1024).toFixed(2)
+              }}GB
+            </text>
+            <text style="margin-left: 10px">{{ this.rateplan.dayStr }}</text>
+          </view>
+          <text>已用:{{ (this.rateplan.used / 1024).toFixed(2) }}GB</text>
+        </view>
+      </view>
+      <view v-if="orderList.length > 0">
+        <view
+          class="progress"
+          flex
+          column
+          center
+          v-for="item in orderList"
+          :key="item.orderId"
+        >
+          <progress
+            activeColor="#FFD5A0"
+            backgroundColor="#746C64"
+            border-radius="20"
+          />
+          <view class="progress-info" flex>
+            <view>
+              <text>待生效流量套餐</text>
+              <text style="margin-left: 10px"
+                >总量:{{ (item.total / 1024 / 1024).toFixed(2) }}GB</text
+              >
+            </view>
+            <text style="margin-left: 10px">{{ item.dayStr }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+    <view class="main">
+      <!--      <button class="button popup-success" @click="getNewAcitivityDetail()">-->
+      <!--        <text-->
+      <!--            class="button-text success-text">领取福利-->
+      <!--        </text>-->
+      <!--      </button>-->
+      <view class="title" flex>
+        <text>流量选择</text>
+        <uni-icons
+          type="help"
+          color="#828282"
+          style="margin-left: 4px"
+          @click="getOpen"
+        />
+      </view>
+      <view flex wrap>
+        <view
+          :class="['list', active === index ? 'active' : '']"
+          v-for="(item, index) in options"
+          :key="index"
+          flex
+          column
+          between
+          @click="getActive(item, index)"
+          :data-content-before="item.operators"
+        >
+          <view class="flow owt">{{ item.name }}</view>
+          <view class="discount icon">{{ item.goodDiscountedPrice }}</view>
+          <view class="price icon">{{ item.goodPrice }}</view>
+          <view v-if="item.tagName" class="tag-name">{{ item.tagName }}</view>
+        </view>
+      </view>
+      <view class="explain" flex column>
+        <text>说明:</text>
+        <text>1、当前流量包购买后,将充值至猫王音响赠送的SIM卡内</text>
+        <text
+          >2、预充值流量因运营商结算时效,上一个流量包使用完后1小时
+          内生效,如有疑问,请咨询客服。</text
+        >
+        <text>3、如在有效期内流量未使用完毕,流量将被清零</text>
+      </view>
+    </view>
+    <view class="footer">
+      <view class="warning">
+        开通后仅支持在当前绑定的猫王音响上通过移动数据模式使用,不支持蓝牙模式
+      </view>
+      <button @click="getWechatPay">立即购买</button>
+    </view>
+
+    <!-- 弹窗 -->
+    <uni-popup
+      ref="popup"
+      type="bottom"
+      border-radius="10px 10px 10px 10px"
+      background-color="#2a2a2a"
+    >
+      <view class="popup">
+        <view class="title">购买须知</view>
+        <view class="content">
+          <h4>
+            1.
+            支付完成后,若当前没有生效中的套餐,套餐立即生效,若当前有生效中未使用完套餐,购买套餐进入流量套餐队列,待上一个套餐失效后激活生效。
+          </h4>
+          <h4>2. 此卡充值流量套餐后,仅限当前设备使用。</h4>
+          <h4>3. 此套餐为虚拟产品,购买后不支持退款。</h4>
+          <h4>
+            4. 客服微信:miao friend
+            ,有任何问题请添加客服微信,我们将耐心为您解决问题。
+          </h4>
+        </view>
+        <button @click="getClose">我知道了</button>
+      </view>
+    </uni-popup>
+
+    <view>
+      <!-- 提示窗示例 -->
+      <uni-popup ref="alertDialog" type="dialog">
+        <uni-popup-dialog
+          :showClose="showClose"
+          :type="msgType"
+          cancelText="不领取"
+          confirmText="领取"
+          title="福利通知"
+          content="您当前有一个流量领取福利!"
+          @confirm="dialogConfirm"
+          @close="dialogClose"
+        ></uni-popup-dialog>
+      </uni-popup>
+    </view>
+    <view>
+      <!-- 提示信息弹窗 -->
+      <uni-popup ref="message" type="message">
+        <uni-popup-message
+          :type="msgType"
+          :message="messageText"
+          :duration="2000"
+        ></uni-popup-message>
+      </uni-popup>
+    </view>
+  </view>
+</template>
+
+<script>
+import {
+detail,
+newAcitivityDetail,
+openId,
+options,
+order,
+wechatPay,
+} from "@/api/pay";
+
+export default {
+  data() {
+    return {
+      type: "center",
+      msgType: "success",
+      messageText: "这是一条成功提示",
+      value: "",
+      showClose: false,
+      // 流量信息
+      info: {},
+      rateplan: {},
+      // 已用流量
+      percent: 0,
+      // 下单表单
+      form: {},
+      // 流量选择
+      options: [],
+      // 选择
+      active: 0,
+      // 流量套餐id
+      goddsId: 0,
+      state: "",
+      // 待生效流量
+      orderList: [],
+      isReceive: false,
+      description: "",
+      id: "",
+      name: "",
+      pic: "",
+    };
+  },
+
+  onLoad(e) {
+    this.state = e.state;
+    this.getOpenId(e);
+    this.getDetail();
+    this.getOptions();
+    this.getOrder();
+    this.getNewAcitivityDetail();
+  },
+  methods: {
+    // 联通流量详细信息
+    getNewAcitivityDetail() {
+      newAcitivityDetail({
+        state: this.state,
+      }).then((res) => {
+        console.log("asdfqwerqrqr====" + JSON.stringify(res));
+        if (res.code === 0) {
+          this.isReceive = res.data.isReceive;
+          this.id = res.data.id;
+          this.name = res.data.name;
+          this.pic = res.data.pic;
+          this.description = res.data.description;
+
+          // 可以领取
+          if (res.data.isReceive) {
+            this.msgType = "success";
+            this.$refs.alertDialog.open();
+          } else {
+            uni.showToast({
+              title: res.data.msg,
+              icon: "none",
+              mask: true,
+              duration: 4000,
+            });
+          }
+        } else {
+          uni.showToast({
+            title: res.message,
+            icon: "none",
+            mask: true,
+            duration: 4000,
+          });
+        }
+      });
+    },
+
+    dialogConfirm() {
+      this.messageText = `点击确认了 ${this.msgType} 窗口`;
+      this.$refs.alertDialog.close();
+      // 跳转到活动页面
+      uni.navigateTo({
+        url: `/pages/activity/new?state=${this.state}&id=${this.id}&pic=${this.pic}`,
+      });
+    },
+    dialogClose() {
+      // alert("关闭")
+    },
+
+    // 获取openId
+    getOpenId(e) {
+      openId(e.code).then((res) => {
+        if (res.code === 0) {
+          uni.setStorageSync("openId", res.data.openid);
+        }
+      });
+    },
+
+    // 流量信息
+    getDetail() {
+      detail({ state: this.state }).then((res) => {
+        if (res.code === 0) {
+          this.info = res.data;
+          this.rateplan = res.data.rateplan;
+          this.percent = (this.rateplan.used / this.rateplan.total) * 100;
+        }
+      });
+    },
+
+    // 流量列表
+    getOrder() {
+      order({ state: this.state }).then((res) => {
+        if (res.code === 0) {
+          this.orderList = res.data.filter((i) => i.status === 1).splice(0, 2);
+        }
+      });
+    },
+
+    // 流量套餐
+    getOptions() {
+      options({ state: this.state }).then((res) => {
+        this.options = res.data;
+        this.goodsId = res.data[0].id;
+        res.data.map((i) =>
+          document.documentElement.style.setProperty("——operators", i.operators)
+        );
+      });
+    },
+
+    // 选择套餐
+    getActive(item, index) {
+      this.active = index;
+      this.goodsId = item.id;
+    },
+
+    // 下单
+    getWechatPay() {
+      wechatPay({
+        openId: uni.getStorageSync("openId"),
+        goodType: 18,
+        goodsId: this.goodsId,
+        ptype: 1,
+        state: this.state,
+        tradeType: 2,
+      }).then((res) => {
+        if (res.code === 0) {
+          this.form = JSON.parse(res.data.wxOrder.formUrl);
+          if (typeof WeixinJSBridge == "undefined") {
+            if (document.addEventListener) {
+              document.addEventListener(
+                "WeixinJSBridge",
+                this.onBridgeReady(),
+                false
+              );
+            } else if (document.attachEvent) {
+              document.attachEvent("WeixinJSBridge", this.onBridgeReady());
+              document.attachEvent(
+                "onWeixinJSBridgeReady",
+                this.onBridgeReady()
+              );
+            }
+          } else {
+            this.onBridgeReady();
+          }
+        }
+      });
+    },
+    // 弹窗
+    getOpen() {
+      this.$refs.popup.open();
+    },
+    // 关闭
+    getClose() {
+      this.$refs.popup.close();
+    },
+    // 调起支付
+    onBridgeReady() {
+      WeixinJSBridge.invoke("getBrandWCPayRequest", this.form, function (res) {
+        if (res.err_msg == "get_brand_wcpay_request:ok") {
+          // 支付成功刷新页面
+          location.reload();
+        }
+      });
+    },
+    // 开通记录
+    getRouter() {
+      uni.navigateTo({
+        url: `/pages/pay/detail?state=${this.state}`,
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 0;
+}
+
+.header {
+  width: 100%;
+  height: 368rpx;
+  background: url("../../static/pay/bg.png") no-repeat 100% / 100%;
+
+  .info {
+    margin-bottom: 38rpx;
+
+    .dev-img {
+      background: rgba(0, 0, 0, 0.12);
+      width: 120rpx;
+      height: 120rpx;
+      border-radius: 50%;
+      margin: 64rpx 0 0 64rpx;
+
+      img {
+        width: 96rpx;
+        height: 96rpx;
+      }
+    }
+
+    .dev {
+      width: calc(590rpx - 130rpx);
+      margin-top: 88rpx;
+      margin-left: 24rpx;
+
+      .dev-name {
+        font-size: 32rpx;
+        margin-bottom: 8rpx;
+      }
+
+      .dev-history {
+        color: #9fa5ad;
+        font-size: 22rpx;
+      }
+    }
+  }
+
+  .progress {
+    width: 100%;
+    margin-bottom: 16rpx;
+
+    ::v-deep uni-progress,
+    .progress-info {
+      width: 590rpx;
+
+      .uni-progress-bar,
+      .uni-progress-inner-bar {
+        border-radius: 18rpx;
+      }
+    }
+
+    .progress-info {
+      font-size: 20rpx;
+      color: #9fa5ad;
+      margin-top: 8rpx;
+    }
+  }
+}
+
+.header-bg {
+  height: 510rpx;
+  background: url("../../static/pay/bigbg.png") no-repeat 100% / 100%;
+}
+
+.main {
+  flex: 1;
+  padding: 0 32rpx;
+  margin-bottom: 320rpx;
+
+  .title {
+    align-items: center;
+  }
+
+  .active {
+    background-color: rgba(199, 170, 134, 0.12);
+  }
+
+  .list {
+    width: calc(100% / 3 - 10px);
+    height: 268rpx;
+    border: 1px solid #c7aa86;
+    border-radius: 16rpx;
+    padding: 48rpx 10px;
+    color: #ffd5a0;
+    position: relative;
+    margin: 24rpx 5px 0;
+
+    .flow {
+      width: 100%;
+      font-size: 28rpx;
+      text-align: center;
+    }
+
+    .discount {
+      font-size: 48rpx;
+    }
+
+    .price {
+      font-size: 24rpx;
+      text-decoration: line-through;
+      opacity: 0.5;
+    }
+
+    .icon::before {
+      content: "¥";
+      font-size: 24rpx;
+    }
+  }
+
+  .list::before {
+    content: attr(data-content-before);
+    position: absolute;
+    left: -1px;
+    top: -1px;
+    background-color: #ffd5a0;
+    color: #765626;
+    font-size: 22rpx;
+    border-radius: 16rpx 0 16rpx 0;
+    padding: 0 16rpx;
+  }
+
+  .tag-name {
+    position: absolute;
+    left: 0px;
+    bottom: -1px;
+    background-color: #ffd5a0;
+    color: #765626;
+    font-size: 22rpx;
+    width: 100%;
+    text-align: center;
+    border-radius: 0 0 16rpx 16rpx;
+  }
+
+  .explain {
+    font-size: 24rpx;
+    opacity: 0.5;
+    margin-top: 24rpx;
+  }
+}
+
+.footer {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  background: #181818;
+
+  .warning {
+    font-size: 22rpx;
+    background-color: #2a2a2a;
+    padding: 16rpx 32rpx;
+  }
+}
+
+.popup {
+  padding: 20px 24px 0;
+
+  .title {
+    text-align: center;
+    font-size: 18px;
+  }
+
+  .content {
+    padding: 16px 0 0 0;
+    font-size: 14px;
+    margin-bottom: 64rpx;
+
+    h4 {
+      font-weight: 100;
+      line-height: 20px;
+      margin-bottom: 10px;
+    }
+  }
+}
+
+button {
+  width: calc(100% - 64rpx);
+  height: 92rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: linear-gradient(90deg, #f3cf97 0%, #f6e5c4 100%);
+  border-radius: 23px;
+  color: #7c541a;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin: 32rpx auto;
+}
+</style>

+ 0 - 22
src/pages/privacy/agreement.vue

@@ -1,22 +0,0 @@
-<template>
-  <view class="app-container">
-
-  </view>
-</template>
-
-<script>
-export default {
-  data() {
-    return {
-
-    }
-  },
-  created(){
-    window.location.href = "https://ohplay.radio1964.net/privacy/protocol"
-  }
-}
-</script>
-
-<style lang="scss">
-
-</style>

+ 4 - 4
src/pages/share/channels.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <view class="app-container">
   <view class="app-container">
-    <open-app :page="0" />
+    <open-app @open="open" />
     <!-- 12频道分享 -->
     <!-- 12频道分享 -->
     <view class="twelve_channels">
     <view class="twelve_channels">
       <view class="item" v-for="item in list" :key="item.channelId" @click="getDetail(item)">
       <view class="item" v-for="item in list" :key="item.channelId" @click="getDetail(item)">
@@ -35,8 +35,8 @@ export default {
       channelsList({
       channelsList({
         userId: e.userId
         userId: e.userId
       }).then(res => {
       }).then(res => {
-        if (res.data.code === 0) {
-          this.list = res.data.data
+        if (res.code === 0) {
+          this.list = res.data
         }
         }
       })
       })
     }
     }
@@ -44,7 +44,7 @@ export default {
   methods: {
   methods: {
     getDetail(item) {
     getDetail(item) {
       uni.navigateTo({
       uni.navigateTo({
-        url: `/pages/share/list?channelId=${item.id}&userId=${this.userId}`,
+        url: `?page=/pages/share/list&channelId=${item.id}&userId=${this.userId}`,
       })
       })
     },
     },
   }
   }

+ 13 - 42
src/pages/share/controls.vue

@@ -1,23 +1,23 @@
 <template>
 <template>
   <div class="app-container">
   <div class="app-container">
     <!-- 背景 -->
     <!-- 背景 -->
-    <view class="bg" :style="{'background': `url(${form.icon})no-repeat 100% / cover`}" />
+    <view class="bg" :style="{ 'background': `url(${form.icon})no-repeat 100% / cover` }" />
     <!-- 跳转App -->
     <!-- 跳转App -->
-    <open-app :page="3" :audioId="form.audioId" />
+    <open-app @open="open" />
     <!-- 播控分享 -->
     <!-- 播控分享 -->
     <view class="play_control">
     <view class="play_control">
       <img v-if="channelType !== '1'" class="cover" :src="form.icon" />
       <img v-if="channelType !== '1'" class="cover" :src="form.icon" />
       <view v-if="channelType === '1'" class="FM">
       <view v-if="channelType === '1'" class="FM">
         <img :src="form.icon" />
         <img :src="form.icon" />
-        <text>以上内容来自{{ form.platformId }}</text>
+        <text>以上内容来自{{ form.platformName }}</text>
       </view>
       </view>
       <view class="title">{{ form.title }}</view>
       <view class="title">{{ form.title }}</view>
       <view class="name" v-if="channelType === '1'">{{ form.title }}</view>
       <view class="name" v-if="channelType === '1'">{{ form.title }}</view>
       <view v-if="channelType !== '1'" class="platform">
       <view v-if="channelType !== '1'" class="platform">
-        以上内容来自{{ form.platformId }}
+        以上内容来自{{ form.platformName }}
       </view>
       </view>
-      <slider min="0" max="100" block-color="#78B06A" block-size="12" activeColor="#78B06A"
-        backgroundColor="#727c82" disabled />
+      <slider min="0" max="100" block-color="#78B06A" block-size="12" activeColor="#78B06A" backgroundColor="#727c82"
+        disabled />
       <view class="length">
       <view class="length">
         <text>00:00:00</text>
         <text>00:00:00</text>
         <text>00:00:00</text>
         <text>00:00:00</text>
@@ -34,34 +34,6 @@ export default {
     return {
     return {
       channelType: '',
       channelType: '',
       form: {},
       form: {},
-      platFormOptions: [{
-        value: 0,
-        label: '未知'
-      }, {
-        value: 1,
-        label: '蜻蜓'
-      }, {
-        value: 2,
-        label: '官方电台'
-      }, {
-        value: 3,
-        label: '猫王好听'
-      }, {
-        value: 4,
-        label: '海外电台'
-      }, {
-        value: 5,
-        label: '音乐随身听'
-      }, {
-        value: 6,
-        label: 'qq音乐'
-      }, {
-        value: 7,
-        label: 'HiFIVE音乐'
-      }, {
-        value: 8,
-        label: '看见音乐'
-      }]
     }
     }
   },
   },
   onLoad(e) {
   onLoad(e) {
@@ -71,19 +43,17 @@ export default {
         boradcastDetail({
         boradcastDetail({
           audioId: e.audioId
           audioId: e.audioId
         }).then(res => {
         }).then(res => {
-          if (res.data.code === 0) {
-            this.form = res.data.data
-            this.form.platformId = this.platFormOptions.find(i => i.value === res.data.data.platformId).label
+          if (res.code === 0) {
+            this.form = res.data
           }
           }
         })
         })
       } else {
       } else {
         podCastProgramDetail({
         podCastProgramDetail({
           audioId: e.audioId
           audioId: e.audioId
         }).then(res => {
         }).then(res => {
-          if (res.data.code === 0) {
-            this.form = res.data.data
-            this.form.platformId = this.platFormOptions.find(i => i.value === res.data.data.platformId).label
-            this.form.icon = res.data.data.audioPic
+          if (res.code === 0) {
+            this.form = res.data
+            this.form.icon = res.data.audioPic
           }
           }
         })
         })
       }
       }
@@ -92,7 +62,8 @@ export default {
   methods: {
   methods: {
     // 打开app
     // 打开app
     open() {
     open() {
-      this.openApp(3, this.form.audioId)
+      // this.openApp(3, this.form.audioId)
+      this.openApp(`?page=/mobile/audioPage&audioId=${this.form.audioId}`)
     }
     }
   }
   }
 }
 }

+ 19 - 16
src/pages/share/detail.vue

@@ -3,7 +3,7 @@
     <!-- 背景 -->
     <!-- 背景 -->
     <view class="bg" />
     <view class="bg" />
     <!-- 跳转App -->
     <!-- 跳转App -->
-    <open-app :page="2" :audioType="data.audioType" />
+    <open-app @open="open" />
     <!-- 详情 -->
     <!-- 详情 -->
     <view class="detail">
     <view class="detail">
       <img class="logo" :src="data.thumb" />
       <img class="logo" :src="data.thumb" />
@@ -27,13 +27,13 @@
     <uni-segmented-control :current="current" @clickItem="onClickItem" styleType="text" :values="items"
     <uni-segmented-control :current="current" @clickItem="onClickItem" styleType="text" :values="items"
       activeColor="#fff" />
       activeColor="#fff" />
     <view v-show="current === 0" class="list_content">
     <view v-show="current === 0" class="list_content">
-      <uni-row class="item" v-for="(item, index) in list" :key="item.id" @click.native="getNav(item)">
-        <uni-col :span="2" style="color:#FFFFFF66; fontSize: 28rpx; fontWeight:bold">
+      <uni-row class="item" v-for="(item, index) in list" :key="item.id" @click.native="open">
+        <uni-col :span="2" style="color:#FFFFFF66; font-size: 28rpx; font-weight:bold">
           {{ index + 1}}
           {{ index + 1}}
         </uni-col>
         </uni-col>
         <uni-col :span="20">
         <uni-col :span="20">
-          <view style="fontSize: 32rpx; margin-bottom: 10rpx;">{{ item.name }}</view>
-          <view style="display:flex; fontSize:22rpx; color:#FFFFFF66;">
+          <view style="font-size: 32rpx; margin-bottom: 10rpx;">{{ item.name }}</view>
+          <view style="display:flex; font-size:22rpx; color:#FFFFFF66;">
             <text class="time">{{ item.durationText }}</text>
             <text class="time">{{ item.durationText }}</text>
             <text class="play">{{ item.playcount }}</text>
             <text class="play">{{ item.playcount }}</text>
             <text class="date">{{ item.updateTimeText }}</text>
             <text class="date">{{ item.updateTimeText }}</text>
@@ -84,9 +84,9 @@ export default {
     // 列表
     // 列表
     getList() {
     getList() {
       podCastProgramList(this.form).then(res => {
       podCastProgramList(this.form).then(res => {
-        if (res.data.code === 0) {
-          this.list = res.data.data.records
-          this.total = res.data.data.total
+        if (res.code === 0) {
+          this.list = res.data.records
+          this.total = res.data.total
         }
         }
       })
       })
     },
     },
@@ -95,20 +95,23 @@ export default {
       podCastDetail({
       podCastDetail({
         audioId: this.form.audioId
         audioId: this.form.audioId
       }).then(res => {
       }).then(res => {
-        if (res.data.code === 0) {
-          this.data = res.data.data
+        if (res.code === 0) {
+          this.data = res.data
         }
         }
       })
       })
     },
     },
     // 下一页
     // 下一页
-    getNav(item) {
-      uni.navigateTo({
-        url: `/pages/share/controls?audioId=${item.audioId}`
-      })
-    },
+    // getNav(item) {
+    //   uni.navigateTo({
+    //     url: `/pages/share/controls?audioId=${item.audioId}`
+    //   })
+    // },
     // 打开app
     // 打开app
     open() {
     open() {
-      this.openApp(2, this.data.audioType)
+      // this.openApp(2, this.data.audioType)
+      if(this.data.audioType === 8){
+        this.openApp(`?page=/mobile/playAlbumPage&audioType=${this.data.audioType}&audioId=${this.form.audioId}`)
+      }
     },
     },
     onClickItem(e) {
     onClickItem(e) {
       if (this.current != e.currentIndex) {
       if (this.current != e.currentIndex) {

+ 31 - 20
src/pages/share/list.vue

@@ -1,9 +1,9 @@
 <template>
 <template>
   <view class="app-container">
   <view class="app-container">
     <!-- 背景 -->
     <!-- 背景 -->
-    <view class="bg" :style="{'background': `url(${data.channelPic})no-repeat 100% / cover`}" />
+    <view class="bg" :style="{ 'background': `url(${data.channelPic})no-repeat 100% / cover` }" />
     <!-- 跳转App -->
     <!-- 跳转App -->
-    <open-app :page="1" :channel="data.channelId" />
+    <open-app @open="open" />
     <!-- 详情 -->
     <!-- 详情 -->
     <view class="detail">
     <view class="detail">
       <img :src="data.channelPic" />
       <img :src="data.channelPic" />
@@ -21,8 +21,8 @@
       </view>
       </view>
     </view>
     </view>
     <!-- 列表 -->
     <!-- 列表 -->
-    <view class="list">
-      <view class="item" v-for="item in list" :key="item.audioId" @click="getNav(item)">
+    <view class="list" v-if="list.length > 0">
+      <view class="item" v-for="item in list" :key="item.audioId" @click="open">
         <img class="logo" :src="item.audioPic" />
         <img class="logo" :src="item.audioPic" />
         <view class="title">
         <view class="title">
           <text>{{ item.audioName }}</text>
           <text>{{ item.audioName }}</text>
@@ -31,6 +31,10 @@
         <img class="play" src="@/static/share/playbtn.png" @click.stop="open" />
         <img class="play" src="@/static/share/playbtn.png" @click.stop="open" />
       </view>
       </view>
     </view>
     </view>
+    <view class="no-data" v-else>
+      <img src="@/static/share/empty.png" style="width: 240rpx; height:240rpx" />
+      <view style="">暂无频道内容哦</view>
+    </view>
   </view>
   </view>
 </template>
 </template>
 
 
@@ -50,15 +54,15 @@ export default {
   },
   },
   onLoad(e) {
   onLoad(e) {
     if (e.userId) {
     if (e.userId) {
-      this.form.userId = e.userId,
+      this.form.userId = e.userId
       this.form.channelId = e.channelId
       this.form.channelId = e.channelId
       // 频道详情
       // 频道详情
       channelDetail({
       channelDetail({
         channelId: e.channelId,
         channelId: e.channelId,
         userId: e.userId
         userId: e.userId
       }).then(res => {
       }).then(res => {
-        if (res.data.code === 0) {
-          this.data = res.data.data
+        if (res.code === 0) {
+          this.data = res.data
         }
         }
       })
       })
       this.getList()
       this.getList()
@@ -74,22 +78,22 @@ export default {
     // 列表
     // 列表
     getList() {
     getList() {
       channelAudioPage(this.form).then(res => {
       channelAudioPage(this.form).then(res => {
-        if (res.data.code === 0) {
-          this.list = res.data.data.records
-          this.total = res.data.data.total
+        if (res.code === 0) {
+          this.list = res.data.records
+          this.total = res.data.total
         }
         }
       })
       })
     },
     },
     // 下一页
     // 下一页
-    getNav(item) {
-      let url = this.data.channelType === 1 || this.data.channelType === 3 ? `/pages/share/controls?channelType=${this.data.channelType}&audioId=${item.audioId}` : `/pages/share/detail?audioId=${item.audioId}`
-      uni.navigateTo({
-        url: url
-      })
-    },
+    // getNav(item) {
+    //   let url = this.data.channelType === 1 || this.data.channelType === 3 ? `/pages/share/controls?channelType=${this.data.channelType}&audioId=${item.audioId}` : `/pages/share/detail?audioId=${item.audioId}`
+    //   uni.navigateTo({
+    //     url: url
+    //   })
+    // },
     // 打开app
     // 打开app
     open() {
     open() {
-      this.openApp(1, this.data.channelId)
+      this.openApp(`?page=/mobile/channelSinglePage&channel=${this.data.channelId}&channelId=${this.data.id}`)
     }
     }
   }
   }
 }
 }
@@ -134,7 +138,6 @@ export default {
     .num {
     .num {
       display: flex;
       display: flex;
       flex-direction: column;
       flex-direction: column;
-      margin-top: -50rpx;
       font-size: 24rpx;
       font-size: 24rpx;
       color: rgba(255, 255, 255, 0.7);
       color: rgba(255, 255, 255, 0.7);
 
 
@@ -159,8 +162,9 @@ export default {
         display: block;
         display: block;
         width: 360rpx;
         width: 360rpx;
         overflow: hidden;
         overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
+        display: -webkit-box;
+        -webkit-box-orient: vertical;
+        -webkit-line-clamp: 3;
       }
       }
     }
     }
 
 
@@ -232,4 +236,11 @@ export default {
     }
     }
   }
   }
 }
 }
+
+.no-data {
+  text-align: center;
+  margin-top: 200rpx;
+  color: rgba(255, 255, 255, 0.7);
+  font-size: 24rpx;
+}
 </style>
 </style>

Різницю між файлами не показано, бо вона завелика
+ 0 - 153
src/pages/user/agreement.vue


Різницю між файлами не показано, бо вона завелика
+ 0 - 375
src/pages/vip/index.vue


+ 24 - 0
src/pages/warning/index.vue

@@ -0,0 +1,24 @@
+<template>
+  <view class='app-container' flex center>
+    {{ msg }}
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return  {
+      msg: ''
+    }
+  },
+  onLoad(e) {
+    this.msg = e.msg
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container{
+  height: 100%;
+}
+</style>

+ 30 - 0
src/pages/ximalaya/error.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class='app-container'>
+    <uni-icons type="clear" color="#FFF" size="68" />
+    <span>支付失败</span>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      
+    }
+  },
+  onLoad() {
+    payStatus.postMessage('{"status": 0}')
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  height: 100%;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 54 - 0
src/pages/ximalaya/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class='app-container'>
+    <uni-icons :class="[title === '加载中...' ? 'spin' : '']" :type="type" color="#FFF" size="68" />
+    <span>{{ title }}</span>
+  </div>
+</template>
+
+<script>
+import { getCallBack } from "@/api/ximalaya.js"
+export default {
+  data() {
+    return {
+      type: 'spinner-cycle',
+      title: '加载中...'
+    }
+  },
+  onLoad(e) {
+    const deviceid = window.location.hash.split('=')[1]
+    getCallBack({ ...e, deviceid: deviceid }).then(res => {
+      if (res.code === 0) {
+        this.type = 'checkbox-filled'
+        this.title = '授权成功!'
+      } else {
+        this.type = 'clear'
+        this.title = '授权失败!'
+      }
+    })
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  height: 100%;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+.spin {
+  animation: spin 5s linear infinite;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg)
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 30 - 0
src/pages/ximalaya/success.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class='app-container'>
+    <uni-icons type="checkbox-filled" color="#FFF" size="68" />
+    <span>支付成功</span>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      
+    }
+  },
+  onLoad() {
+    payStatus.postMessage('{"status": 1}')
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  height: 100%;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+</style>

BIN
src/static/address.png


BIN
src/static/location.png


BIN
src/static/museum/detail/4.png


BIN
src/static/museum/detail/5.png


BIN
src/static/museum/detail/6.png


BIN
src/static/pay/bg.png


BIN
src/static/pay/bigbg.png


BIN
src/static/pay/light.png


BIN
src/static/pay/mask.png


BIN
src/static/phone.png


BIN
src/static/play.png


BIN
src/static/share/empty.png


BIN
src/static/stop.png


+ 39 - 0
src/utils/openApp.js

@@ -0,0 +1,39 @@
+const openApp = function (url) {
+  const res = uni.getSystemInfoSync()
+  var ua = window.navigator.userAgent.toLowerCase();
+  if (ua.match(/MicroMessenger/i) == 'micromessenger' || ua.match(/WeiBo/i) == "weibo" || ua.match(/QQ/i) == "qqbrowser ") {
+    uni.showToast({
+      icon: 'none',
+      title: '右上角在浏览器打开',
+      mask: true
+    })
+  } else {
+    if (res.platform === 'ios') {
+      window.location.href = `airsmart://${url}&cmd=openPage`
+    } else {
+      window.location.href = `airsmart://muzen${url}&cmd=openPage`
+    }
+
+    var timer = setTimeout(() => {
+      if (res.platform === 'ios') {
+        window.location.href = 'itms-appss://itunes.apple.com/cn/app/apple-store/id1621419943?mt=8'
+      } else {
+        window.location.href = 'http://download.radio1964.com/readme?'
+      }
+    }, 3000)
+  }
+
+  document.addEventListener('visibilitychange', function () {
+    if (document.hidden) {
+      clearTimeout(timer)
+    }
+  })
+
+  window.onpagehide = event => {
+    if (event.persisted) {
+      clearTimeout(timer)
+    }
+  }
+}
+
+export default openApp;

+ 40 - 0
src/utils/parseTime.js

@@ -0,0 +1,40 @@
+// 日期格式化
+export function parseTime(time, pattern) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+      time = parseInt(time)
+    } else if (typeof time === 'string') {
+      time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
+    }
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+    let value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+    if (result.length > 0 && value < 10) {
+      value = '0' + value
+    }
+    return value || 0
+  })
+  return time_str
+}

+ 4 - 5
src/utils/request.js

@@ -1,22 +1,21 @@
-const baseUrl = 'https://pre.client.ohplay.radio1964.net/web' // 预生产
-
 function request(e) {
 function request(e) {
   if (!e.data) {
   if (!e.data) {
     e.data = {}
     e.data = {}
   }
   }
 
 
   uni.showLoading({
   uni.showLoading({
-    title: '加载中...'
+    title: '加载中...',
+    mask: true
   })
   })
 
 
   const promise = new Promise((resolve, reject) => {
   const promise = new Promise((resolve, reject) => {
     uni.request({
     uni.request({
-      url: baseUrl + e.url,
+      url: process.env.VUE_APP_BASE_API + e.url,
       header: e.header,
       header: e.header,
       method: e.method,
       method: e.method,
       data: e.data,
       data: e.data,
       success: (res) => {
       success: (res) => {
-        resolve(res)
+        resolve(res.data)
         uni.hideLoading()
         uni.hideLoading()
       },
       },
       fail: (err) => {
       fail: (err) => {

+ 21 - 2
vue.config.js

@@ -1,3 +1,22 @@
+const port = process.env.port || process.env.npm_config_port || 80 // 端口
+
 module.exports = {
 module.exports = {
-  transpileDependencies: ['@zebra-ui']
-}
+  transpileDependencies: ['@zebra-ui'],
+  // webpack-dev-server 相关配置
+  devServer: {
+    host: '0.0.0.0',
+    port: port,
+    open: true,
+    proxy: {
+      // detail: https://cli.vuejs.org/config/#devserver-proxy
+      [process.env.VUE_APP_BASE_API]: {
+        target: `https://o3tapi.radio1964.com/web`,
+        changeOrigin: true,
+        pathRewrite: {
+          ['^' + process.env.VUE_APP_BASE_API]: ''
+        }
+      }
+    },
+    disableHostCheck: true
+  }
+}