li.shaoyi há 1 ano atrás
pai
commit
0fe71e83a5

+ 2 - 2
public/manifest.json

@@ -5,9 +5,9 @@
     "name" : "铁合金掌上行",
     /*应用名称,程序桌面图标名称*/
     "version" : {
-        "name" : "1.1.0",
+        "name" : "1.1.1",
         /*应用版本名称*/
-        "code" : 100100
+        "code" : 100101
     },
     "description" : "",
     /*应用描述信息*/

+ 19 - 0
src/filters/index.ts

@@ -229,4 +229,23 @@ export function encryptDesMobile(mobile: string) {
 export function getEncryptMobile(mobile: string) {
     const encryptMobile = encryptDesMobile(mobile)
     return encryptMobile.replace(new RegExp('\\+', 'g'), '*').replace(new RegExp('\\/', 'g'), '-').replace(new RegExp('\\=', 'g'), '.')
+}
+
+/**
+ * 版本号转数值
+ * @param value 
+ * @returns 
+ */
+export function versionToNumber(value: number | string) {
+    if (value) {
+        const num = value.toString().split(/\D/)
+        // 版本号位数
+        const place = ['', '0', '00', '000', '0000', '00000', '000000'].reverse()
+        for (let i = 0; i < num.length; i++) {
+            const len = num[i].length
+            num[i] = place[len] + num[i]
+        }
+        return +num.join('')
+    }
+    return 0
 }

+ 16 - 5
src/hooks/component/index.ts

@@ -1,15 +1,19 @@
 import { ref, Ref, onUnmounted } from 'vue'
 import { onBeforeRouteLeave } from 'vue-router'
 import { v4 } from 'uuid'
+import { useLoginStore } from '@/stores'
 
 // 缓存已打开的组件实例
 const componentInstanceMap = new Map<string, Ref>()
 
 /**
+ * 
  * @param callback 组件关闭时的回调
+ * @param routeListener 是否监听路由离开
  * @returns 
  */
-export function useComponent(callback?: (componentName?: string) => void) {
+export function useComponent(callback?: (componentName?: string) => void, routeListener = true) {
+    const loginStore = useLoginStore()
     const uuid = v4()
     const components = new Set<string>() // 已打开的组件列表
     const componentId = ref<string>() // 当前显示的组件
@@ -31,7 +35,9 @@ export function useComponent(callback?: (componentName?: string) => void) {
      * @param componentName 
      */
     const openComponent = (componentName: string) => {
-        components.add(componentName)
+        if (routeListener) {
+            components.add(componentName)
+        }
         componentId.value = componentName
     }
 
@@ -80,10 +86,15 @@ export function useComponent(callback?: (componentName?: string) => void) {
      * 路由守卫,离开页面前关闭组件
      */
     onBeforeRouteLeave((to, from, next) => {
-        if (closeComponentEach()) {
-            next()
+        if ((to.meta.ignoreAuth && from.meta.ignoreAuth) || loginStore.token) {
+            if (closeComponentEach()) {
+                next()
+            } else {
+                next(false)
+            }
         } else {
-            next(false)
+            components.clear()
+            next()
         }
     })
 

+ 6 - 6
src/hooks/navigation/index.ts

@@ -72,15 +72,11 @@ export function useNavigation() {
     const backHome = () => {
         const state = animateRouter.getState()
         const delta = state.historyRoutes.length - 1
-        const params = { tabIndex: 0 }
-
-        if (delta) {
-            setGlobalUrlParams(params)
+        if (delta > 0) {
             router.go(-delta)
         } else {
             const page = state.historyRoutes[0]
             if (page?.name !== 'home-index') {
-                setGlobalUrlParams(params)
                 router.replace({ name: 'home-index' })
             }
         }
@@ -89,7 +85,11 @@ export function useNavigation() {
     // 返回上个页面
     const routerBack = <T extends object>(params?: T) => {
         setGlobalUrlParams(params)
-        router.back()
+        if (hasHistory()) {
+            router.back()
+        } else {
+            router.replace({ path: '/' })
+        }
     }
 
     // 路由跳转

+ 1 - 3
src/packages/mobile/App.vue

@@ -17,10 +17,8 @@ eventBus.$on('LogoutNotify', (msg) => {
     if (msg) {
       dialog({
         message: msg as string,
+        confirmButtonText: '确定'
       }).then(() => {
-        // ---待处理---
-        // 登出后应该回退到首页,如果回退后非首页,会导致路由拦截而跳转到登录页面,此时因为 tabIndex = 0 的问题,登录页被 replace 成首页,导致路由还能继续后退
-        // 临时解决方案是先退回首页后再进行登出操作
         backHome()
       })
     } else {

+ 44 - 0
src/packages/mobile/components/base/notify/index.less

@@ -0,0 +1,44 @@
+.notify-in-leave-to,
+.notify-in-enter-active,
+.notify-out-leave-active,
+.notify-out-enter-to {
+    pointer-events: none;
+    transition-property: transform, opacity;
+    transition-duration: 300ms;
+}
+
+.notify-in-enter-from,
+.notify-out-leave-active {
+    opacity: .75;
+    transform: translate3d(0, -100%, 0);
+}
+
+.app-notify {
+    position: fixed;
+    top: 0;
+    z-index: 1000;
+    width: 100%;
+
+    &__container {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        font-size: .3rem;
+        padding: .32rem;
+    }
+
+    &__content {
+        width: 100%;
+        line-height: normal;
+        background-color: #fff;
+        box-shadow: 0 0 0.32rem rgba(0, 0, 0, .15);
+        border-radius: .32rem;
+        padding: .48rem .64rem;
+
+        h4 {
+            font-weight: bold;
+            margin-bottom: .16rem;
+        }
+    }
+}

+ 55 - 0
src/packages/mobile/components/base/notify/index.vue

@@ -0,0 +1,55 @@
+<template>
+    <teleport to="body">
+        <transition :name="transitionName">
+            <app-statusbar class="app-notify" v-if="show">
+                <div class="app-notify__container" @click="onClose">
+                    <div class="app-notify__content">
+                        <h4>
+                            <slot name="title">{{ title }}</slot>
+                        </h4>
+                        <p>
+                            <slot>{{ content }}</slot>
+                        </p>
+                    </div>
+                </div>
+            </app-statusbar>
+        </transition>
+    </teleport>
+</template>
+
+<script lang="ts" setup>
+import { computed, watch } from 'vue'
+
+const props = defineProps({
+    show: {
+        type: Boolean,
+        default: false
+    },
+    // 展示时长(ms),值为 0 时,notify 不会消失
+    duration: {
+        type: Number,
+        default: 3000
+    },
+    title: String,
+    content: String
+})
+
+const emit = defineEmits(['update:show'])
+const transitionName = computed(() => props.show ? 'notify-in' : 'notify-out')
+
+const onClose = () => {
+    emit('update:show', false)
+}
+
+watch(() => props.show, (isShow) => {
+    if (isShow && props.duration) {
+        window.setTimeout(() => {
+            onClose()
+        }, props.duration)
+    }
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 36 - 0
src/packages/mobile/components/base/updater/index.less

@@ -0,0 +1,36 @@
+.app-updater {
+    &-message {
+        font-size: .32rem;
+        text-align: center;
+    }
+
+    &-progress {
+        position: relative;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 100%;
+        height: .48rem;
+        background-color: #ebedf0;
+        border-radius: .24rem;
+        overflow: hidden;
+
+        &__bar {
+            position: absolute;
+            left: 0;
+            display: inline-block;
+            height: 100%;
+            background-color: #ff8400;
+        }
+
+        &__text {
+            z-index: 1;
+            font-size: .24rem;
+            color: #fff;
+        }
+    }
+
+    .van-dialog__content {
+        padding: .64rem;
+    }
+}

+ 127 - 0
src/packages/mobile/components/base/updater/index.vue

@@ -0,0 +1,127 @@
+<template>
+    <Dialog teleport="body" class="app-updater" v-model:show="show" :title="message ? '提示' : '下载中'"
+        :show-confirm-button="showConfirmButton" :confirm-button-text="confirmButtonText" show-cancel-button
+        :before-close="onbeforeClose" @confirm="onConfirm" @cancel="onCancel">
+        <div class="app-updater-message" v-if="message">
+            <span>{{ message }}</span>
+        </div>
+        <div class="app-updater-progress" v-else>
+            <span class="app-updater-progress__bar" :style="styles"></span>
+            <span class="app-updater-progress__text">{{ downloadProgress.toFixed(0) }}%</span>
+        </div>
+    </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, computed, onMounted, onUnmounted } from 'vue'
+import { Dialog } from 'vant'
+import { versionToNumber } from '@/filters'
+import { GetAppUpdateInfo } from '@/services/api/common'
+import plus from '@/utils/h5plus'
+
+const props = defineProps({
+    iosUpdateUrl: String
+})
+
+const show = shallowRef(false)
+const pending = shallowRef(true) // 是否进行中状态
+const showConfirmButton = shallowRef(true)
+const message = shallowRef('')
+const confirmButtonText = shallowRef('')
+const downloadUrl = shallowRef('') // 下载地址
+const downloadProgress = shallowRef(0) // 下载进度
+const fileUrl = shallowRef('') // 文件安装地址
+
+const styles = computed(() => ({
+    width: downloadProgress.value + '%'
+}))
+
+// 监听下载进度
+const ondownload = plus.onDownloadProgress((progress) => {
+    downloadProgress.value = progress
+})
+
+const onbeforeClose = () => {
+    return false
+}
+
+const onConfirm = () => {
+    const os = plus.getSystemInfo('os')
+    if (os === 'iOS') {
+        show.value = false
+        plus.openURL(downloadUrl.value)
+    } else if (downloadProgress.value === 100) {
+        show.value = false
+        plus.installApp(fileUrl.value)
+    } else if (confirmButtonText.value === '隐藏') {
+        show.value = false
+    } else {
+        message.value = ''
+        confirmButtonText.value = '隐藏'
+        plus.createDownload(downloadUrl.value).then((res) => {
+            fileUrl.value = res.filename
+            message.value = '下载完成,是否立即安装?'
+            confirmButtonText.value = '安装'
+        }).catch((err) => {
+            message.value = err
+            showConfirmButton.value = false
+        }).finally(() => {
+            show.value = pending.value
+        })
+    }
+}
+
+const onCancel = () => {
+    if (fileUrl.value) {
+        plus.deleteFile(fileUrl.value)
+    }
+    ondownload.cancel()
+    pending.value = false
+    show.value = false
+}
+
+onMounted(() => {
+    const os = plus.getSystemInfo('os')
+    const currentVersion = plus.getSystemInfo('version')
+    const currentVersionCode = plus.getSystemInfo('versionCode')
+
+    if (os === 'Android') {
+        // 获取应用更新信息
+        GetAppUpdateInfo().then((res) => {
+            const data = JSON.parse(res)
+            if (data) {
+                const { LastVersionCode, ApkUrl } = data[0] as Model.AppUpdateInfo
+                if (Number(LastVersionCode) > Number(currentVersionCode)) {
+                    downloadUrl.value = ApkUrl
+                    message.value = '发现新版本,是否更新?'
+                    confirmButtonText.value = '下载'
+                    show.value = true
+                }
+            }
+        })
+    }
+
+    if (os === 'iOS' && props.iosUpdateUrl) {
+        plus.httpRequest({
+            url: props.iosUpdateUrl
+        }).then((res) => {
+            const results = res.data.results
+            if (results?.length) {
+                const { version, trackViewUrl } = results[0]
+                if (versionToNumber(version) > versionToNumber(currentVersion)) {
+                    downloadUrl.value = trackViewUrl
+                    message.value = '发现新版本,是否更新?'
+                    confirmButtonText.value = '更新'
+                    show.value = true
+                }
+            }
+        })
+    }
+})
+
+onUnmounted(() => ondownload.cancel())
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 13 - 0
src/packages/mobile/components/base/uploader/index.less

@@ -0,0 +1,13 @@
+.app-uploader {
+    position: relative;
+
+    &__button {
+        position: absolute;
+        left: 0;
+        top: 0;
+        z-index: 1;
+        display: block;
+        width: 100%;
+        height: 100%;
+    }
+}

+ 55 - 7
src/packages/mobile/components/base/uploader/index.vue

@@ -1,23 +1,67 @@
 <template>
-    <Uploader v-model="fileList" :max-count="1" :max-size="5 * 1024 * 1024" @oversize="onOversize" :after-read="afterRead"
-        @delete="onDelete" />
+    <div class="app-uploader">
+        <span class="app-uploader__button" @click="onClickUpload"></span>
+        <Uploader ref="uploaderRef" v-model="fileList" :max-count="maxCount" :max-size="5 * 1024 * 1024"
+            @oversize="onOversize" :after-read="onAfterRead" @delete="onDelete" />
+        <Notify v-model:show="showNotify" :duration="0" title="相册权限使用说明" content="用于更换头像等场景" />
+    </div>
 </template>
 
 <script lang="ts" setup>
 import { ref } from 'vue'
 import { showFailToast, Uploader, UploaderFileListItem } from 'vant'
+import { useGlobalStore } from '@/stores'
+import plus from '@/utils/h5plus'
 import service from '@/services'
 import axios from 'axios'
+import Notify from '@mobile/components/base/notify/index.vue'
 
-const emit = defineEmits(['success'])
+defineProps({
+    maxCount: {
+        type: Number,
+        default: 1
+    }
+})
+
+const emit = defineEmits(['success', 'delete'])
+const globalStore = useGlobalStore()
+const uploaderRef = ref()
+const showNotify = ref(false)
 const fileList = ref<UploaderFileListItem[]>([])
 
 const onOversize = () => {
     showFailToast('图片大小不能超过 5Mb')
 }
 
+const onClickUpload = () => {
+    const status = globalStore.getAndroidPermissions('READ_EXTERNAL_STORAGE')
+    switch (status) {
+        case 0: {
+            showNotify.value = true
+            plus.requestPermissionReadExternalStorage({
+                onSuccess: () => {
+                    showNotify.value = false
+                    globalStore.setAndroidPermissions('READ_EXTERNAL_STORAGE', 1)
+                },
+                onError: () => {
+                    showNotify.value = false
+                    globalStore.setAndroidPermissions('READ_EXTERNAL_STORAGE', -1)
+                }
+            })
+            break
+        }
+        case 1: {
+            uploaderRef.value.chooseFile()
+            break
+        }
+        default: {
+            showFailToast('相册权限未授权')
+        }
+    }
+}
+
 // eslint-disable-next-line
-const afterRead = (file: any) => {
+const onAfterRead = (file: any) => {
     const data = new FormData()
     data.append('file', file.file)
 
@@ -37,7 +81,11 @@ const afterRead = (file: any) => {
     })
 }
 
-const onDelete = () => {
-    emit('success', '')
+const onDelete = (file: UploaderFileListItem, detail: { name: string; index: number; }) => {
+    emit('delete', detail.index)
 }
-</script>
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 23 - 10
src/packages/mobile/router/animateRouter.ts

@@ -1,13 +1,13 @@
 import { ref, toRefs } from 'vue'
 import { createRouter, RouterOptions, RouteRecordRaw, RouteLocationNormalized } from 'vue-router'
 
-interface historyRoute {
+interface HistoryRoute {
     name: string;
     fullPath: string;
 }
 
 interface HistoryState {
-    historyRoutes: historyRoute[]; // 历史路由列表
+    historyRoutes: HistoryRoute[]; // 历史路由列表
     excludeViews: string[]; // 不缓存的视图
     actionName: '' | 'push' | 'replace' | 'forward' | 'back'; // 当前路由动作
     transitionName: '' | 'route-out' | 'route-in'; // 前进后退动画
@@ -115,10 +115,10 @@ export default new (class {
         // 如果是替换动作,必定是前进
         if (actionName.value === 'replace') {
             const lastIndex = historyRoutes.value.length - 1
-            const lastView = historyRoutes.value[lastIndex]
+            const lastRecord = historyRoutes.value[lastIndex]
 
-            if (lastView) {
-                excludeViews.value.push(lastView.name as string)
+            if (lastRecord) {
+                excludeViews.value.push(lastRecord.name as string)
                 historyRoutes.value[lastIndex] = newRoute // 更新最后一条记录
             } else {
                 historyRoutes.value.push(newRoute)
@@ -152,14 +152,27 @@ export default new (class {
                 }
             } else {
                 if (goStep.value < 0) {
-                    const start = historyRoutes.value.length + goStep.value
-                    if (start) {
-                        historyRoutes.value.splice(start)
+                    const i = historyRoutes.value.length + goStep.value
+                    if (i > 0) {
+                        const n = historyRoutes.value.length - i
+                        excludeViews.value = historyRoutes.value.map((e) => e.name).slice(-n) // 返回数组最后位置开始的n个元素
+                        historyRoutes.value.splice(i)
                     }
                 }
-                // 忽略重定向的页面
+                // 如有存在重定向的页面,判断路由前进还是后退
                 if (route.redirectedFrom) {
-                    transitionName.value = 'route-in' // 前进动画
+                    if (goStep.value < 0) {
+                        const index = historyRoutes.value.length - 1
+                        const lastIndex = index > -1 ? index : 0
+                        historyRoutes.value[lastIndex] = newRoute // 更新最后一条记录
+                        transitionName.value = 'route-out' //后退动画
+                    } else {
+                        const [lastRecord] = historyRoutes.value.slice(-1)
+                        if (route.redirectedFrom.fullPath !== lastRecord?.fullPath) {
+                            historyRoutes.value.push(newRoute)
+                        }
+                        transitionName.value = 'route-in' // 前进动画
+                    }
                 } else {
                     historyRoutes.value.push(newRoute)
                     if (historyRoutes.value.length > 1) {

+ 12 - 4
src/packages/mobile/router/index.ts

@@ -568,10 +568,18 @@ router.beforeEach((to, from, next) => {
     if (to.meta.ignoreAuth || loginStore.token) {
       next();
     } else {
-      next({
-        name: "UserLogin",
-        query: { redirect: to.fullPath },
-      });
+      if (to.matched.some((e) => e.name === 'home')) {
+        // 如果是主页导航页面,强制跳转到首页
+        next({
+          name: 'home-index',
+          replace: true,
+        })
+      } else {
+        next({
+          name: 'UserLogin',
+          query: { redirect: to.fullPath },
+        })
+      }
     }
   } else {
     if (to.name === "Boot" || to.name === "UserLogin") {

+ 17 - 96
src/packages/mobile/views/home/index.vue

@@ -9,27 +9,29 @@
       </RouterTransition>
     </router-view>
     <app-tabbar class="home-tabbar" :data-list="tabList" :data-index="currentTab" @click="onTabClick" />
+    <app-updater ios-update-url="https://itunes.apple.com/lookup?id=1661067556" />
   </div>
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, nextTick, watch, onMounted } from 'vue'
-import { fullloading, dialog } from '@/utils/vant'
+import { shallowRef, nextTick, watch, onMounted, computed } from 'vue'
+import { fullloading } from '@/utils/vant'
 import { Tabbar } from '@mobile/components/base/tabbar/interface'
-import { GetAppUpdateInfo } from '@/services/api/common'
 import { useNavigation } from '@/hooks/navigation'
 import { useLogin } from '@/business/login'
 import { useLoginStore } from '@/stores'
-import plus from '@/utils/h5plus'
 import AppTabbar from '@mobile/components/base/tabbar/index.vue'
+import AppUpdater from '@mobile/components/base/updater/index.vue'
 import RouterTransition from '@mobile/components/base/router-transition/index.vue'
 
-const { route, routerTo, getGlobalUrlParams } = useNavigation()
+const { route, routerTo } = useNavigation()
 const { userLogin } = useLogin()
 const loginStore = useLoginStore()
 const cssTransition = shallowRef(true) // 是否使用css动画
 const currentTab = shallowRef(0)
 
+const tabIndex = computed(() => tabList.findIndex((e) => e.name === route.name))
+
 const tabList: Tabbar[] = [
   {
     name: 'home-index',
@@ -53,15 +55,11 @@ const tabList: Tabbar[] = [
 
 const onTabClick = (index: number) => {
   const { name } = tabList[index]
-  cssTransition.value = false
-
-  if (name === 'home-index' || loginStore.token) {
-    currentTab.value = index
+  if (index === 0 || loginStore.token) {
     routerTo(name, true)
   } else {
     fullloading((hideLoading) => {
       userLogin(true).then(() => {
-        currentTab.value = index
         routerTo(name, true)
       }).catch(() => {
         routerTo('UserLogin')
@@ -72,95 +70,18 @@ const onTabClick = (index: number) => {
   }
 }
 
-// 版本号转数值
-const versionToNumber = (value: number | string) => {
-  if (value) {
-    const num = value.toString().split(/\D/)
-    // 版本号位数
-    const place = ['', '0', '00', '000', '0000', '00000', '000000'].reverse()
-    for (let i = 0; i < num.length; i++) {
-      const len = num[i].length
-      num[i] = place[len] + num[i]
-    }
-    return +num.join('')
-  }
-  return 0
-}
-
-onMounted(() => {
-  const { tabIndex } = getGlobalUrlParams()
-  currentTab.value = tabIndex > -1 ? tabIndex : tabList.findIndex((e) => e.name === route.name)
-
-  const os = plus.getSystemInfo('os')
-  const currentVersion = plus.getSystemInfo('version')
-  const currentVersionCode = plus.getSystemInfo('versionCode')
-
-  if (os === 'Android') {
-    // 监听下载进度
-    const ondownload = plus.onDownload((filename, progress) => {
-      if (progress === 100) {
-        dialog({
-          message: '新版本下载完成,是否安装?',
-          showCancelButton: true,
-          confirmButtonText: '安装'
-        }).then(() => {
-          plus.installApp(filename)
-        }).catch(() => {
-          plus.deleteFile(filename)
-        })
-      }
-    })
-
-    // 获取应用更新信息
-    GetAppUpdateInfo().then((res) => {
-      const data = JSON.parse(res)
-      if (data) {
-        const { LastVersionCode, ApkUrl } = data[0] as Model.AppUpdateInfo
-        if (Number(LastVersionCode) > Number(currentVersionCode)) {
-          dialog({
-            message: '发现新版本,是否下载?',
-            showCancelButton: true,
-            confirmButtonText: '下载'
-          }).then(() => {
-            plus.createDownload(ApkUrl)
-          }).catch(() => {
-            ondownload.cancel()
-          })
-        }
-      }
-    })
-  }
-
-  if (os === 'iOS') {
-    plus.httpRequest({
-      url: 'https://itunes.apple.com/lookup?id=1661067556'
-    }).then((res) => {
-      const results = res.data.results
-      if (results?.length) {
-        const { version, trackViewUrl } = results[0]
-        if (versionToNumber(version) > versionToNumber(currentVersion)) {
-          dialog({
-            message: '发现新版本,是否更新?',
-            showCancelButton: true,
-            confirmButtonText: '更新'
-          }).then(() => {
-            plus.openURL(trackViewUrl)
-          })
-        }
-      }
-    })
+watch(() => route.name, () => {
+  if (tabIndex.value > -1) {
+    cssTransition.value = false
+    currentTab.value = tabIndex.value
   }
+  nextTick(() => {
+    cssTransition.value = true
+  })
 })
 
-watch(() => route.name, () => {
-  const { tabIndex } = getGlobalUrlParams()
-  if (tabIndex > -1) {
-    onTabClick(tabIndex)
-  } else {
-    nextTick(() => {
-      cssTransition.value = true
-    })
-  }
+onMounted(() => {
+  currentTab.value = tabIndex.value
 })
 </script>
 

+ 1 - 1
src/packages/mobile/views/home/main/index.vue

@@ -54,7 +54,7 @@
         <section class="scrollbar">
           <Swipe :autoplay="8000" :show-indicators="false" :touchable="false" vertical @change="onSpotChange">
             <SwipeItem v-for="(item, i) in spotQuoteList" :key="i">
-              <ul @click="openNewsDetails(item.relatedid)">
+              <ul>
                 <li>
                   <span>{{ item.spotsrc }}</span>
                 </li>

+ 1 - 1
src/packages/mobile/views/user/login/index.less

@@ -1,7 +1,7 @@
 .login {
     display: flex;
     flex-direction: column;
-    background: url('@mobile/assets/images/login-bg.jpg') no-repeat center top;
+    background:#fff url('@mobile/assets/images/login-bg.jpg') no-repeat center top;
     background-size: 100% 100%;
 
     &-navback {

+ 17 - 0
src/stores/modules/global.ts

@@ -6,12 +6,27 @@ import plus from '@/utils/h5plus'
 
 export const useGlobalStore = defineStore(() => {
     const appTheme = localData.getRef('appTheme')
+    const androidPermissions = localData.getRef('androidPermissions')
 
     const state = reactive({
         clientWidth: 0, // 客户端宽度
         isMobile: false, // 是否移动设备
     })
 
+    // 获取APP权限状态
+    const getAndroidPermissions = <K extends keyof typeof androidPermissions.value>(key: K) => {
+        if (plus.hasPlus()) {
+            return androidPermissions.value[key]
+        }
+        return 1
+    }
+
+    // 更新APP权限状态
+    const setAndroidPermissions = <K extends keyof typeof androidPermissions.value>(key: K, value: number) => {
+        androidPermissions.value[key] = value
+        localData.setValue('androidPermissions', androidPermissions.value)
+    }
+
     // 设置状态栏主题色
     const setStatusBarTheme = (theme: AppTheme) => {
         switch (theme) {
@@ -103,6 +118,8 @@ export const useGlobalStore = defineStore(() => {
     return {
         ...toRefs(state),
         appTheme,
+        getAndroidPermissions,
+        setAndroidPermissions,
         setStatusBarTheme,
         setTheme,
         screenAdapter,

+ 5 - 0
src/stores/storage.ts

@@ -8,6 +8,11 @@ function createLocalData() {
         appTheme: AppTheme.Default,
         loginInfo: <Proto.LoginRsp | undefined>undefined,
         autoLoginEncryptedData: '', // 自动登录加密数据
+        androidPermissions: {
+            READ_EXTERNAL_STORAGE: 0, // -1 永久拒绝,0 未授权,1已授权
+            CAMERA: 0,
+            RECORD_AUDIO: 0
+        }
     }
 }
 

+ 171 - 55
src/utils/h5plus/index.ts

@@ -1,6 +1,6 @@
 /* eslint-disable */
 import { v4 } from 'uuid'
-import { SystemInfo, ShareMessage, HttpRequestConfig } from './interface'
+import { SystemInfo, ShareMessage, HttpRequestConfig, Download } from './types'
 import { urlScheme } from './constants'
 
 declare global {
@@ -9,6 +9,11 @@ declare global {
     }
 }
 
+interface AndroidErrorCallback {
+    code: number;
+    message: string;
+}
+
 export default new (class {
     private readonly plusready = new Promise<void>((resolve) => {
         if (this.hasPlus()) {
@@ -24,16 +29,16 @@ export default new (class {
     private xhr = new XMLHttpRequest()
 
     /**
-     * 当前下载任务
+     * 当前下载进度任务
      */
-    private downloadTask = new Map()
+    private progressTask = new Map()
 
     /**
      * 系统信息
      */
     private systemInfo: SystemInfo = {
         os: 'Web', // 客户端操作系统
-        version: '1.0', // 客户端版本号
+        version: '1.0.0', // 客户端版本号
         versionCode: '100000', // 客户端版本代码
         statusBarHeight: 0, // 状态栏高度
     }
@@ -53,26 +58,30 @@ export default new (class {
         // 监听返回按钮事件
         this.onPlusReady((plus) => {
             let firstBack = true
-            const webview = plus.webview.currentWebview()
-
             plus.key.addEventListener('backbutton', () => {
-                webview.canBack((e: any) => {
-                    // 判断能否继续返回
-                    if (e.canBack) {
-                        webview.back()
-                    } else {
-                        // 1秒内连续两次按返回键退出应用
-                        if (firstBack) {
-                            firstBack = false
-                            plus.nativeUI.toast('再按一次退出应用')
-                            setTimeout(() => {
-                                firstBack = true
-                            }, 1000)
+                const webviews = plus.webview.all() // 所有Webview窗口
+                if (webviews.length > 1) {
+                    plus.webview.close(webviews[webviews.length - 1])
+                } else {
+                    const webview = plus.webview.currentWebview()
+                    webview.canBack((e: any) => {
+                        // 判断能否继续返回
+                        if (e.canBack) {
+                            webview.back()
                         } else {
-                            plus.runtime.quit()
+                            // 1秒内连续两次按返回键退出应用
+                            if (firstBack) {
+                                firstBack = false
+                                plus.nativeUI.toast('再按一次退出应用')
+                                setTimeout(() => {
+                                    firstBack = true
+                                }, 1000)
+                            } else {
+                                plus.runtime.quit()
+                            }
                         }
-                    }
-                })
+                    })
+                }
             })
         })
     }
@@ -214,14 +223,14 @@ export default new (class {
      * @param callback 
      * @returns 
      */
-    onDownload(callback: (filename: string, progress: number) => void) {
+    onDownloadProgress(callback: (progress: number) => void) {
         const uuid = v4()
-        this.downloadTask.set(uuid, callback)
+        this.progressTask.set(uuid, callback)
 
         /** 注意离开页面时销毁监听事件,防止事件重复触发 */
         return {
             uuid,
-            cancel: () => this.downloadTask.delete(uuid)
+            cancel: () => this.progressTask.delete(uuid)
         }
     }
 
@@ -231,39 +240,39 @@ export default new (class {
      * @param url 
      */
     createDownload(url: string) {
-        this.onPlusReady((plus) => {
-            // plus.downloader.enumerate((downloads: any) => {
-            //     if (downloads.length) {
-            //         plus.nativeUI.toast('正在下载')
-            //     } else {
-
-            //     }
-            // })
-            const task = plus.downloader.createDownload(url, {
-                filename: '_downloads/', // 非系统 Download 目录
-                retry: 1,
-            }, (d: any, status: number) => {
-                if (status !== 200) {
-                    plus.nativeUI.toast('下载失败')
-                }
-            })
-            // 监听下载状态
-            task.addEventListener('statechanged', (task: any) => {
-                switch (task.state) {
-                    case 3:
-                        const progress = task.downloadedSize / task.totalSize * 100
-                        for (const fn of this.downloadTask.values()) {
-                            fn(task.filename, progress) // 推送下载进度
+        return new Promise<Download>((resolve, reject) => {
+            this.onPlusReady((plus) => {
+                // plus.downloader.enumerate((downloads: any) => {
+                //     if (downloads.length) {
+                //         plus.nativeUI.toast('正在下载')
+                //     } else {
+
+                //     }
+                // })
+                const task = plus.downloader.createDownload(url, {
+                    filename: '_downloads/', // 非系统 Download 目录
+                    retry: 1,
+                }, (download: Download, status: number) => {
+                    if (status === 200) {
+                        resolve(download)
+                    } else {
+                        reject('下载失败,请稍后再试')
+                    }
+                    this.progressTask.clear()
+                })
+                // 监听下载状态
+                task.addEventListener('statechanged', (e: Download) => {
+                    //console.log(e.state, e.downloadedSize / e.totalSize * 100)
+                    if (e.state === 3) {
+                        const progress = e.downloadedSize / e.totalSize * 100
+                        for (const fn of this.progressTask.values()) {
+                            fn(progress) // 推送下载进度
                         }
-                        break
-                    case 4:
-                        console.log('下载完成', task.filename)
-                        this.downloadTask.clear()
-                        break
-                }
+                    }
+                })
+                // 开始下载
+                task.start()
             })
-            // 开始下载
-            task.start()
         })
     }
 
@@ -334,6 +343,30 @@ export default new (class {
     }
 
     /**
+     * https://www.html5plus.org/doc/zh_cn/webview.html#plus.webview.create
+     * @param options 
+     */
+    openWebview(options: { url: string; id?: string; titleText?: string; titleColor?: string; backgroundColor?: string; onClose?: () => void; }) {
+        if (this.hasPlus()) {
+            const styles = {
+                titleNView: {
+                    backgroundColor: options.backgroundColor,
+                    titleText: options.titleText,
+                    titleColor: options.titleColor,
+                    autoBackButton: true,
+                }
+            }
+            this.onPlusReady((plus) => {
+                const wv = plus.webview.create(options.url, options.id ?? v4(), styles)
+                wv.show()
+                wv.addEventListener('close', () => options.onClose && options.onClose(), false)
+            })
+        } else {
+            this.openURL(options.url)
+        }
+    }
+
+    /**
      * 将本地URL路径转换成平台绝对路径
      * https://www.html5plus.org/doc/zh_cn/io.html#plus.io.convertLocalFileSystemURL
      * @param url 
@@ -454,4 +487,87 @@ export default new (class {
             })
         })
     }
+
+    /**
+     * 检查运行环境的权限
+     * https://www.html5plus.org/doc/zh_cn/navigator.html#plus.navigator.checkPermission
+     */
+    checkPermission() {
+        this.onPlusReady((plus) => {
+            const res = plus.navigator.checkPermission('android.permission.READ_EXTERNAL_STORAGE')
+
+            console.log(res)
+        })
+    }
+
+    /**
+     * 请求APP权限
+     */
+    requestPermission(options: Partial<{
+        permissionId: string;
+        errorMessage?: string;
+        refuseMessage?: string;
+        onSuccess?: () => void;
+        onError?: (message: string) => void;
+    }>) {
+        const { onSuccess, onError } = options
+        if (this.hasPlus()) {
+            this.onPlusReady((plus) => {
+                plus.android.requestPermissions([options.permissionId], (e: { granted: string[]; deniedPresent: string[]; deniedAlways: string[]; }) => {
+                    if (e.deniedAlways.length > 0) {
+                        onError && onError(options.refuseMessage ?? '')
+                    }
+                    if (e.deniedPresent.length > 0) {
+                        onError && onError(options.errorMessage ?? '')
+                    }
+                    if (e.granted.length > 0) {
+                        onSuccess && onSuccess()
+                    }
+                }, (e: AndroidErrorCallback) => {
+                    onError && onError(e.message)
+                })
+            })
+        } else {
+            onSuccess && onSuccess()
+        }
+    }
+
+    /**
+     * 请求存储权限
+     */
+    requestPermissionReadExternalStorage(options: Partial<{ onSuccess: () => void; onError: (message: string) => void; }> = {}) {
+        this.requestPermission({
+            permissionId: 'android.permission.READ_EXTERNAL_STORAGE',
+            errorMessage: '请打开存储权限',
+            refuseMessage: '访问存储被拒绝',
+            onSuccess: options.onSuccess,
+            onError: options.onError
+        })
+    }
+
+    /**
+     * 请求摄像头权限
+     */
+    requestPermissionCamera(options: Partial<{ onSuccess: () => void; onError: (message: string) => void; }> = {}) {
+        this.requestPermission({
+            permissionId: 'android.permission.CAMERA',
+            errorMessage: '请打开摄像头权限',
+            refuseMessage: '访问摄像头被拒绝',
+            onSuccess: options.onSuccess,
+            onError: options.onError
+        })
+    }
+
+    /**
+     * 请求麦克风权限
+     */
+    requestPermissionRecordAudio(options: Partial<{ onSuccess: () => void; onError: (message: string) => void; }> = {}) {
+        this.requestPermission({
+            permissionId: 'android.permission.RECORD_AUDIO',
+            errorMessage: '访问麦克风被拒绝',
+            refuseMessage: '请打开麦克风权限',
+            onSuccess: options.onSuccess,
+            onError: options.onError
+        })
+    }
 })

+ 9 - 0
src/utils/h5plus/interface.ts → src/utils/h5plus/types.ts

@@ -59,4 +59,13 @@ export interface HttpRequestConfig {
     method?: string;
     responseType?: XMLHttpRequestResponseType;
     header?: { [key: string]: string };
+}
+
+export interface Download {
+    id: string;
+    url: string;
+    state: number;
+    filename: string;
+    downloadedSize: number;
+    totalSize: number;
 }