/* eslint-disable */ import { v4 } from 'uuid' import { SystemInfo, ShareMessage, HttpRequestConfig } from './types' import { urlScheme } from './constants' declare global { interface Window { plus: any; } } interface AndroidErrorCallback { code: number; message: string; } export default new (class { private readonly plusready = new Promise((resolve) => { if (this.hasPlus()) { resolve() } else { document.addEventListener('plusready', () => resolve()) } }) /** * 网络请求对象 */ private xhr = new XMLHttpRequest() /** * 当前下载任务 */ private downloadTask = new Map() /** * 系统信息 */ private systemInfo: SystemInfo = { os: 'Web', // 客户端操作系统 version: '1.0.0', // 客户端版本号 versionCode: '100000', // 客户端版本代码 statusBarHeight: 0, // 状态栏高度 } constructor() { this.onPlusReady((plus) => { this.xhr = new plus.net.XMLHttpRequest() this.systemInfo.os = plus.os.name this.systemInfo.statusBarHeight = plus.navigator.getStatusbarHeight() plus.runtime.getProperty(plus.runtime.appid, (info: any) => { this.systemInfo.version = info.version this.systemInfo.versionCode = info.versionCode }) }) // 监听返回按钮事件 this.onPlusReady((plus) => { let firstBack = true plus.key.addEventListener('backbutton', () => { 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 { // 1秒内连续两次按返回键退出应用 if (firstBack) { firstBack = false plus.nativeUI.toast('再按一次退出应用') setTimeout(() => { firstBack = true }, 1000) } else { plus.runtime.quit() } } }) } }) }) } hasPlus() { return !!window.plus } onPlusReady(callback: (plus: Window['plus']) => void) { this.plusready.then(() => { callback(window.plus) }) } /** * 退出应用程序 */ quit() { this.onPlusReady((plus) => { plus.runtime.quit() }) } /** * 获取系统信息 * @param prop * @returns */ getSystemInfo(prop: K) { return this.systemInfo[prop] } /** * 获取状态栏高度 * @param callback */ getStatusBarHeight(callback: (statusbarHeight: number) => void) { this.onPlusReady((plus) => { const height = plus.navigator.getStatusbarHeight() callback(height) }) } /** * 设置状态栏文字颜色 * @param color dark - 暗色,light - 亮色 */ setStatusBarStyle(color: 'dark' | 'light') { this.onPlusReady((plus) => { plus.navigator.setStatusBarStyle(color) }) } /** * 隐藏状态栏 */ hideStatusBar() { this.onPlusReady((plus) => { plus.navigator.setFullscreen(true) }) } /** * 显示状态栏 */ showStatusBar() { this.onPlusReady((plus) => { plus.navigator.setFullscreen(false) }) } /** * 设置应用全屏 */ setFullSreen() { this.onPlusReady((plus) => { this.hideStatusBar() plus.navigator.hideSystemNavigation() }) } /** * 应用退出全屏 */ exitFullSreen() { this.onPlusReady((plus) => { this.showStatusBar() plus.navigator.showSystemNavigation() }) } /** * http 跨域请求(待完善) * @param config * @returns */ httpRequest(config: HttpRequestConfig) { return new Promise((resolve, reject) => { this.xhr.responseType = config.responseType ?? 'json' if (config.header) { for (const key in config.header) { this.xhr.setRequestHeader(key, config.header[key]) } } this.xhr.onreadystatechange = () => { if (this.xhr.readyState === 4) { if (this.xhr.status == 200) { resolve({ code: 200, data: this.xhr.response }) } else { reject({ code: this.xhr.status, message: this.xhr.statusText }) } } } this.xhr.open(config.method ?? 'GET', config.url) this.xhr.send() }) } /** * 删除文件 * @param url */ deleteFile(url: string) { this.onPlusReady((plus) => { plus.io.resolveLocalFileSystemURL(url, (entry: any) => { entry.remove() }) }) } /** * 监听下载进度 * @param callback * @returns */ onDownload(callback: (filename: string, progress: number) => void) { const uuid = v4() this.downloadTask.set(uuid, callback) /** 注意离开页面时销毁监听事件,防止事件重复触发 */ return { uuid, cancel: () => this.downloadTask.delete(uuid) } } /** * 文件下载 * https://www.html5plus.org/doc/zh_cn/downloader.html#plus.downloader.createDownload * @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) => { console.log(task.state, task.downloadedSize / task.totalSize * 100) switch (task.state) { case 3: const progress = task.downloadedSize / task.totalSize * 100 for (const fn of this.downloadTask.values()) { fn(task.filename, progress) // 推送下载进度 } break case 4: console.log('下载完成', task.filename) this.downloadTask.clear() break } }) // 开始下载 task.start() }) } /** * App更新安装 * @param file */ installApp(file: string) { this.onPlusReady((plus) => { plus.nativeUI.showWaiting('正在安装...') plus.runtime.install(file, { // true表示强制安装,不进行版本号的校验;false则需要版本号校验,如果将要安装应用的版本号不高于现有应用的版本号则终止安装,并返回安装失败。 仅安装wgt和wgtu时生效,默认值 false force: false }, () => { console.log('安装成功!') this.deleteFile(file) plus.nativeUI.closeWaiting() plus.runtime.restart() }, (e: any) => { plus.nativeUI.closeWaiting() plus.nativeUI.alert('安装失败:' + e.message) }) }) } /** * 保存图片到相册 * @param base64Data */ saveImage(base64Data: string, fileName?: string) { this.onPlusReady((plus) => { const bitmap = new plus.nativeObj.Bitmap() const filename = fileName ?? new Date().getTime() bitmap.loadBase64Data(base64Data) bitmap.save(`_doc/${filename}.jpg`, { overwrite: true, quality: 100, }, (e: Event) => { //保存到系统相册 plus.gallery.save( e.target, () => { //销毁Bitmap图片 bitmap.clear() plus.nativeUI.toast('已保存到相册中') }, () => { //销毁Bitmap图片 bitmap.clear() plus.nativeUI.toast('保存失败') } ) }, (err: { message: string }) => { plus.nativeUI.toast(err.message) }) }) } /** * https://www.html5plus.org/doc/zh_cn/runtime.html#plus.runtime.openURL * @param url */ openURL(url: string) { if (this.hasPlus()) { this.onPlusReady((plus) => { plus.runtime.openURL(url) }) } else { window.open(url) } } /** * 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 */ convertLocalFileSystemURL(url: string) { return new Promise((resolve) => { if (this.hasPlus()) { this.onPlusReady((plus) => { const localURL = plus.io.convertLocalFileSystemURL('_www/' + url) resolve(localURL) }) } else { const absoluteURL = new URL(url, window.location.href).href resolve(absoluteURL) } }) } /** * 读取本地文件内容 * https://www.html5plus.org/doc/zh_cn/io.html#plus.io.resolveLocalFileSystemURL * @param filePath * @returns */ getLocalFileContent(filePath: string) { return new Promise((resolve, reject) => { this.onPlusReady((plus) => { plus.io.resolveLocalFileSystemURL('_www/' + filePath, (entry: any) => { entry.file((file: any) => { const fileReader = new plus.io.FileReader() fileReader.readAsText(file, 'utf-8') fileReader.onloadend = (evt: any) => { resolve(evt.target.result) } }) }, (e: any) => { reject(e.message) }) }) }) } /** * 系统分享 * https://www.html5plus.org/doc/zh_cn/share.html#plus.share.sendWithSystem * @param message * @returns */ systemShare(message: Partial) { return new Promise((resolve, reject) => { this.onPlusReady((plus) => { const logo = plus.io.convertLocalFileSystemURL('./logo.gif') const shareMessage: Partial = { type: 'web', thumbs: ['file://' + logo], ...message, } plus.share.sendWithSystem(shareMessage, () => { resolve() }, (e: { code: number; message: string; }) => { reject(e.message) }) }) }) } /** * 内容分享 * https://www.html5plus.org/doc/zh_cn/share.html#plus.share.getServices */ share() { this.onPlusReady((plus) => { // 成功回调 const success = (services: any) => { console.log('分享列表', services) services.forEach((e: any) => { if (e.id === 'weixin') { e.send({ type: 'web', title: '标题', content: '内容', href: 'https://', }, () => { console.log('分享成功') }, (e: Error) => { console.log('分享失败', e.message) }) } }) } // 失败回调 const error = (e: Error) => { console.log('获取分享列表失败', e.message) } plus.share.getServices(success, error) }) } /** * 打开第三方APP * https://www.html5plus.org/doc/zh_cn/runtime.html#plus.runtime.launchApplication * @param app */ launchApplication(app: K) { this.onPlusReady((plus) => { const os = this.getSystemInfo('os') const params = Object.create(null) if (os === 'Android') { params.pname = urlScheme[app].pname } if (os === 'iOS') { params.action = urlScheme[app].scheme } plus.runtime.launchApplication(params, (e: Error) => { console.log('失败', e.message) }) }) } /** * 请求摄像头权限 */ requestPermissionCamera(options: Partial<{ onSuccess: () => void; onError: (message: string) => void; }> = {}) { const { onSuccess, onError } = options if (this.hasPlus()) { this.onPlusReady((plus) => { plus.android.requestPermissions(['android.permission.CAMERA'], (e: { granted: string[]; deniedPresent: string[]; deniedAlways: string[]; }) => { if (e.deniedAlways.length > 0) { onError && onError('访问摄像头被拒绝') } if (e.deniedPresent.length > 0) { onError && onError('请打开摄像头权限') } if (e.granted.length > 0) { onSuccess && onSuccess() } }, (e: AndroidErrorCallback) => { onError && onError(e.message) }) }) } else { onSuccess && onSuccess() } } /** * 请求麦克风权限 */ requestPermissionRecordAudio(options: Partial<{ onSuccess: () => void; onError: (message: string) => void; }> = {}) { const { onSuccess, onError } = options if (this.hasPlus()) { this.onPlusReady((plus) => { plus.android.requestPermissions(['android.permission.RECORD_AUDIO'], (e: { granted: string[]; deniedPresent: string[]; deniedAlways: string[]; }) => { if (e.deniedAlways.length > 0) { onError && onError('访问麦克风被拒绝') } if (e.deniedPresent.length > 0) { onError && onError('请打开麦克风权限') } if (e.granted.length > 0) { onSuccess && onSuccess() } }, (e: AndroidErrorCallback) => { onError && onError(e.message) }) }) } else { onSuccess && onSuccess() } } })