Browse Source

feat(XWebview): 添加文件下载功能

- 实现了文件下载的逻辑,包括进度显示和错误处理
- 新增了下载文件的UI展示,显示下载进度和状态
- 优化了导入模块,增加了request和common模块的导入
- 修复了一些代码格式问题,优化了代码结构
徐勤民 3 weeks ago
parent
commit
7b1ce19c39
1 changed files with 329 additions and 265 deletions
  1. 329 265
      src/main/ets/pages/XWebview.ets

+ 329 - 265
src/main/ets/pages/XWebview.ets

@@ -3,8 +3,8 @@ import webview from '@ohos.web.webview';
 import { router, WebHeader } from '@kit.ArkUI';
 import { XDialogController } from '../dialog/XDialogController';
 import { XDialogList } from '../dialog/XDialogList';
-import { picker } from '@kit.CoreFileKit';
-import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
+import { fileUri, picker } from '@kit.CoreFileKit';
+import { BusinessError, pasteboard, request } from '@kit.BasicServicesKit';
 import { ToolsHelper } from '../utils/ToolsHelper';
 import { JsParams, XWebController, XWebParams } from '../utils/XWebHelper';
 import { WindowHelper } from '../utils/WindowHelper';
@@ -13,6 +13,8 @@ import { SZYXLocalStorageKeys } from '../utils/SZYXLocalStorageKeys';
 import { XWebManager } from '../utils/XWebManager';
 import { TitleBarBtn } from '../view/SafeView';
 import { GlobalContext } from '../ContextConfig';
+import { common } from '@kit.AbilityKit';
+import { LogHelper } from '../utils/LogHelper';
 
 @Entry({ routeName: 'XWebview' })
 @Component
@@ -37,6 +39,9 @@ export struct XWebview {
   controller: web_webview.WebviewController = new web_webview.WebviewController();
   dialogController: XDialogController = {} as XDialogController
   ports: webview.WebMessagePort[] = [];
+  @State isDownload: boolean = false
+  @State receivedSize: number = 0
+  @State totalSize: number = 0
 
   onRefresh() {
     if (this.refresh_web_base !== -1) {
@@ -101,170 +106,228 @@ export struct XWebview {
   }
 
   build() {
-    Column() {
-
-      Row() {
+    Stack() {
+      Column() {
         Row() {
-          Button({ buttonStyle: ButtonStyleMode.TEXTUAL }) {
-            Image($r('sys.media.ohos_ic_back'))
-              .width(26).height(26)
-          }.onClick(() => {
-            this.onBackPress()
-          })
-
-          Button({ buttonStyle: ButtonStyleMode.TEXTUAL }) {
-            Image($r('sys.media.ohos_ic_public_close'))
-              .width(26).height(26)
-          }.margin({ left: 12 })
-          .onClick(() => {
-            router.back()
-          })
-        }.width(65)
+          Row() {
+            Button({ buttonStyle: ButtonStyleMode.TEXTUAL }) {
+              Image($r('sys.media.ohos_ic_back'))
+                .width(26).height(26)
+            }.onClick(() => {
+              this.onBackPress()
+            })
 
-        Text(this.title)
-          .maxLines(1)
-          .fontColor('#222222')
-          .fontSize(18)
-          .textAlign(TextAlign.Center)
-          .width('50%')
-          .ellipsisMode(EllipsisMode.END)
-          .textOverflow({
-            overflow: TextOverflow.Ellipsis
-          })
+            Button({ buttonStyle: ButtonStyleMode.TEXTUAL }) {
+              Image($r('sys.media.ohos_ic_public_close'))
+                .width(26).height(26)
+            }.margin({ left: 12 })
+            .onClick(() => {
+              router.back()
+            })
+          }.width(65)
 
-        Button({ buttonStyle: ButtonStyleMode.TEXTUAL }) {
-          if (this.clickMenu?.img) {
-            Image(this.clickMenu?.img)
-              .width(15).height(15).objectFit(ImageFit.Contain)
-          } else if (this.clickMenu?.text) {
-            Text(this.clickMenu.text)
-              .fontColor(this.clickMenu?.color ?? '#17171A')
-              .fontSize(13)
-              .textAlign(TextAlign.Center)
-              .ellipsisMode(EllipsisMode.END)
-              .textOverflow({
-                overflow: TextOverflow.Ellipsis
-              })
-          } else {
-            Image($r('sys.media.ohos_ic_public_more'))
-              .width(26).height(26)
-          }
-        }.width(65)
-        .onClick(() => {
-          if (this.clickMenu) {
-            XWebManager.menuClick()
-            return
-          }
-          if (this.dialogController != null) {
-            this.dialogController.open()
-          }
-        }).visibility(this.showMenu || this.clickMenu ? Visibility.Visible : Visibility.Hidden)
-      }
-      .width('100%')
-      .height(45)
-      .justifyContent(FlexAlign.SpaceBetween)
-      .padding({ left: 15, right: 15 })
+          Text(this.title)
+            .maxLines(1)
+            .fontColor('#222222')
+            .fontSize(18)
+            .textAlign(TextAlign.Center)
+            .width('50%')
+            .ellipsisMode(EllipsisMode.END)
+            .textOverflow({
+              overflow: TextOverflow.Ellipsis
+            })
 
-      Row().backgroundColor('#CCCCCC').height(2).width('100%')
-      Progress({ value: this.progress, type: ProgressType.Linear })
-        .visibility(this.progress > 95 || this.progress == 0 ? Visibility.None : Visibility.Visible)
+          Button({ buttonStyle: ButtonStyleMode.TEXTUAL }) {
+            if (this.clickMenu?.img) {
+              Image(this.clickMenu?.img)
+                .width(15).height(15).objectFit(ImageFit.Contain)
+            } else if (this.clickMenu?.text) {
+              Text(this.clickMenu.text)
+                .fontColor(this.clickMenu?.color ?? '#17171A')
+                .fontSize(13)
+                .textAlign(TextAlign.Center)
+                .ellipsisMode(EllipsisMode.END)
+                .textOverflow({
+                  overflow: TextOverflow.Ellipsis
+                })
+            } else {
+              Image($r('sys.media.ohos_ic_public_more'))
+                .width(26).height(26)
+            }
+          }.width(65)
+          .onClick(() => {
+            if (this.clickMenu) {
+              XWebManager.menuClick()
+              return
+            }
+            if (this.dialogController != null) {
+              this.dialogController.open()
+            }
+          }).visibility(this.showMenu || this.clickMenu ? Visibility.Visible : Visibility.Hidden)
+        }
         .width('100%')
+        .height(45)
+        .justifyContent(FlexAlign.SpaceBetween)
+        .padding({ left: 15, right: 15 })
 
-      Web({ src: this.url ?? '', controller: this.controller })
-        .javaScriptAccess(true)
-        .javaScriptProxy({
-          object: XWebManager.objs.get(this._uuidToHtml),
-          name: this.jsParams?.name,
-          methodList: this.jsParams?.methodList,
-          controller: this.controller
-        })
-        .domStorageAccess(true)
-        .geolocationAccess(true)
-        .width('100%')
-        .height('100%')
-        .visibility(this.errorInfo == null ? Visibility.Visible : Visibility.None)
-        .mixedMode(MixedMode.All)//允许加载HTTP和HTTPS混合内容
-        .zoomAccess(this.zoomAccess ?? false)//不支持手势进行缩放
-        .mediaPlayGestureAccess(false)//有声视频播放不需要用户手动点击
-        .cacheMode(CacheMode.None)//设置缓存模式
-        .onConfirm((event) => { // 自定义Confirm弹窗
-          if (event) {
-            console.log("event.url:" + event.url)
-            console.log("event.message:" + event.message)
-            AlertDialog.show({
-              title: '提示',
-              message: event.message,
-              primaryButton: {
-                value: '取消',
-                action: () => {
+        Row().backgroundColor('#CCCCCC').height(2).width('100%')
+        Progress({ value: this.progress, type: ProgressType.Linear })
+          .visibility(this.progress > 95 || this.progress == 0 ? Visibility.None : Visibility.Visible)
+          .width('100%')
+
+        Web({ src: this.url ?? '', controller: this.controller })
+          .javaScriptAccess(true)
+          .javaScriptProxy({
+            object: XWebManager.objs.get(this._uuidToHtml),
+            name: this.jsParams?.name,
+            methodList: this.jsParams?.methodList,
+            controller: this.controller
+          })
+          .domStorageAccess(true)
+          .geolocationAccess(true)
+          .width('100%')
+          .height('100%')
+          .visibility(this.errorInfo == null ? Visibility.Visible : Visibility.None)
+          .mixedMode(MixedMode.All)//允许加载HTTP和HTTPS混合内容
+          .zoomAccess(this.zoomAccess ?? false)//不支持手势进行缩放
+          .mediaPlayGestureAccess(false)//有声视频播放不需要用户手动点击
+          .cacheMode(CacheMode.None)//设置缓存模式
+          .onConfirm((event) => { // 自定义Confirm弹窗
+            if (event) {
+              console.log("event.url:" + event.url)
+              console.log("event.message:" + event.message)
+              AlertDialog.show({
+                title: '提示',
+                message: event.message,
+                primaryButton: {
+                  value: '取消',
+                  action: () => {
+                    event.result.handleCancel()
+                  }
+                },
+                secondaryButton: {
+                  value: '确定',
+                  action: () => {
+                    event.result.handleConfirm()
+                  }
+                },
+                cancel: () => {
                   event.result.handleCancel()
                 }
-              },
-              secondaryButton: {
-                value: '确定',
-                action: () => {
-                  event.result.handleConfirm()
-                }
-              },
-              cancel: () => {
-                event.result.handleCancel()
-              }
-            })
-          }
-          return true
-        })
-        .onAlert((event) => { // 自定义Alert弹窗
-          if (event) {
-            console.log("event.url:" + event.url)
-            console.log("event.message:" + event.message)
-            AlertDialog.show({
-              title: '提示',
-              message: event.message,
-              secondaryButton: {
-                value: '确定',
-                action: () => {
-                  event.result.handleConfirm()
+              })
+            }
+            return true
+          })
+          .onAlert((event) => { // 自定义Alert弹窗
+            if (event) {
+              console.log("event.url:" + event.url)
+              console.log("event.message:" + event.message)
+              AlertDialog.show({
+                title: '提示',
+                message: event.message,
+                secondaryButton: {
+                  value: '确定',
+                  action: () => {
+                    event.result.handleConfirm()
+                  }
                 }
+              })
+            }
+            return true
+          })
+          .onDownloadStart((event) => { // 下载文件
+            if (event) {
+              this.isDownload = true
+              let context = getContext(this) as common.UIAbilityContext;
+
+              let filesDir = context.filesDir;
+              const filePath =
+                `${filesDir}/${ToolsHelper.getUuid()}.${event.mimetype ? event.mimetype.split('/')[1] : ''}`
+
+              try {
+                request.downloadFile(context, {
+                  url: event.url,
+                  filePath: filePath
+                }).then((downloadTask: request.DownloadTask) => {
+                  downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
+                    this.receivedSize = receivedSize
+                    this.totalSize = totalSize
+                  })
+                  downloadTask.on('complete', () => {
+                    this.isDownload = false
+                    LogHelper.info(filePath);
+                    GlobalContext.getContext().startAbility({
+                      uri: fileUri.getUriFromPath(filePath),
+                      type: event.mimetype,
+                      flags: 0x00000002
+                    })
+                  })
+                  downloadTask.on('fail', () => {
+                    this.isDownload = false
+                    ToolsHelper.showMessage('下载失败')
+                  })
+                }).catch((err: BusinessError) => {
+                  this.isDownload = false
+                  LogHelper.error(`Invoke downloadTask failed, code is ${err.code}, message is ${err.message}`);
+                  ToolsHelper.showMessage(err.message)
+                });
+              } catch (error) {
+                this.isDownload = false
+                let err: BusinessError = error as BusinessError;
+                LogHelper.error(`Invoke downloadFile failed, code is ${err.code}, message is ${err.message}`);
+                ToolsHelper.showMessage(err.message)
               }
-            })
-          }
-          return true
-        })
-        .onDownloadStart((event) => { // 下载文件
-          if (event) {
-            console.log('url:' + event.url)
-            console.log('userAgent:' + event.userAgent)
-            console.log('contentDisposition:' + event.contentDisposition)
-            console.log('contentLength:' + event.contentLength)
-            console.log('mimetype:' + event.mimetype)
+              // ToolsHelper.log("")
+              // ToolsHelper.log('url:' + event.url)
+              // ToolsHelper.log('userAgent:' + event.userAgent)
+              // ToolsHelper.log('contentDisposition:' + event.contentDisposition)
+              // ToolsHelper.log('contentLength:' + event.contentLength)
+              // ToolsHelper.log('mimetype:' + event.mimetype)
 
-          }
-        })
-        .onPageEnd((url) => {
-          ToolsHelper.log(url.url)
-          // 1、创建两个消息端口。
-          this.ports = this.controller.createWebMessagePorts();
-          this.ports[1].onMessageEvent((result: webview.WebMessage) => {
-            if (typeof (result) === 'string') {
-              // ToolsHelper.log(result, typeof (result))
-              XWebManager.sendMessage(result)
             }
           })
-          this.controller.postMessage('__init_port__', [this.ports[0]], '*');
-        })
-        .onControllerAttached(() => {
-          if (this.jsParams?.controller && this._uuidToHtml) {
-            XWebManager.addOnMessageToWeb(this._uuidToHtml, (msg) => {
-              this.controller.runJavaScript(msg)
+          .onPageEnd((url) => {
+            ToolsHelper.log(url.url)
+            // 1、创建两个消息端口。
+            this.ports = this.controller.createWebMessagePorts();
+            this.ports[1].onMessageEvent((result: webview.WebMessage) => {
+              if (typeof (result) === 'string') {
+                // ToolsHelper.log(result, typeof (result))
+                XWebManager.sendMessage(result)
+              }
             })
-          }
-          if (this.content) {
-            try {
-              this.controller.loadData(this.content, 'text/html', 'UTF-8', " ", " ")
-            } catch (e) {
+            this.controller.postMessage('__init_port__', [this.ports[0]], '*');
+          })
+          .onControllerAttached(() => {
+            if (this.jsParams?.controller && this._uuidToHtml) {
+              XWebManager.addOnMessageToWeb(this._uuidToHtml, (msg) => {
+                this.controller.runJavaScript(msg)
+              })
+            }
+            if (this.content) {
+              try {
+                this.controller.loadData(this.content, 'text/html', 'UTF-8', " ", " ")
+              } catch (e) {
+                ToolsHelper.showAlertDialog({
+                  title: '警告',
+                  msg: e.message ?? '加载内容失败',
+                  action: {
+                    onClick: () => {
+                      router.back()
+                    }
+                  }
+                })
+              }
+            } else if (this.url) {
+              if (this.headers) {
+                this.controller.loadUrl(this.url, this.headers);
+              } else {
+                this.controller.loadUrl(this.url);
+              }
+            } else {
               ToolsHelper.showAlertDialog({
                 title: '警告',
-                msg: e.message ?? '加载内容失败',
+                msg: '请传入url或content',
                 action: {
                   onClick: () => {
                     router.back()
@@ -272,134 +335,135 @@ export struct XWebview {
                 }
               })
             }
-          } else if (this.url) {
-            if (this.headers) {
-              this.controller.loadUrl(this.url, this.headers);
+            try {
+              let userAgent = this.controller.getUserAgent() + '/szyx_sdk';
+              this.controller.setCustomUserAgent(userAgent);
+
+            } catch (error) {
+              console.error(`ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
+            }
+          })
+          .onErrorReceive((event) => { // 加载失败
+            if (this.progress > 65) {
+              return
+            }
+            if (event) {
+              this.errorInfo = `错误码:${event.error.getErrorCode()}\n${event.error.getErrorInfo()}`
             } else {
-              this.controller.loadUrl(this.url);
+              this.errorInfo = '错误码:-1\n未知错误'
+            }
+            ToolsHelper.log(JSON.stringify(event), this.url)
+          })
+          .onHttpErrorReceive((event) => { // 加载失败
+            if (this.progress > 65) {
+              return
             }
-          } else {
-            ToolsHelper.showAlertDialog({
-              title: '警告',
-              msg: '请传入url或content',
-              action: {
-                onClick: () => {
-                  router.back()
-                }
-              }
-            })
-          }
-          try {
-            let userAgent = this.controller.getUserAgent() + '/szyx_sdk';
-            this.controller.setCustomUserAgent(userAgent);
-
-          } catch (error) {
-            console.error(`ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
-          }
-        })
-        .onErrorReceive((event) => { // 加载失败
-          if (this.progress > 65) {
-            return
-          }
-          if (event) {
-            this.errorInfo = `错误码:${event.error.getErrorCode()}\n${event.error.getErrorInfo()}`
-          } else {
-            this.errorInfo = '错误码:-1\n未知错误'
-          }
-          ToolsHelper.log(JSON.stringify(event), this.url)
-        })
-        .onHttpErrorReceive((event) => { // 加载失败
-          if (this.progress > 65) {
-            return
-          }
-          if (event) {
-            this.errorInfo = `错误码:${event.response.getResponseCode()}\n${event.response.getReasonMessage()}`
-          } else {
-            this.errorInfo = '错误码:-1\n未知错误'
-          }
-          ToolsHelper.log(this.errorInfo, this.url)
-        })
-        .onProgressChange((event) => { // 加载进度
-          if (event) {
-            console.log('newProgress:' + event.newProgress)
-            this.progress = event.newProgress
-          }
-        })
-        .onTitleReceive((event) => {
-          // 如果没有传输title,则从H5获取title赋值
-          if (event && !this.title) {
-            this.title = event.title
-          }
-        })
-        .onShowFileSelector((event) => { // 选择文件
-          console.log('MyFileUploader onShowFileSelector invoked')
-          const documentSelectOptions = new picker.DocumentSelectOptions()
-          let uri: string | null = null;
-          const documentViewPicker = new picker.DocumentViewPicker();
-          documentViewPicker.select(documentSelectOptions).then((documentSelectResult) => {
-            uri = documentSelectResult[0];
-            console.info('documentViewPicker.select to file succeed and uri is:' + uri);
             if (event) {
-              event.result.handleFileList([uri]);
+              this.errorInfo = `错误码:${event.response.getResponseCode()}\n${event.response.getReasonMessage()}`
+            } else {
+              this.errorInfo = '错误码:-1\n未知错误'
             }
-          }).catch((err: BusinessError) => {
+            ToolsHelper.log(this.errorInfo, this.url)
+          })
+          .onProgressChange((event) => { // 加载进度
             if (event) {
-              event.result.handleFileList([])
+              console.log('newProgress:' + event.newProgress)
+              this.progress = event.newProgress
+            }
+          })
+          .onTitleReceive((event) => {
+            // 如果没有传输title,则从H5获取title赋值
+            if (event && !this.title) {
+              this.title = event.title
             }
-            ToolsHelper.showMessage(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`)
-            console.error(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
           })
-          return true
+          .onShowFileSelector((event) => { // 选择文件
+            console.log('MyFileUploader onShowFileSelector invoked')
+            const documentSelectOptions = new picker.DocumentSelectOptions()
+            let uri: string | null = null;
+            const documentViewPicker = new picker.DocumentViewPicker();
+            documentViewPicker.select(documentSelectOptions).then((documentSelectResult) => {
+              uri = documentSelectResult[0];
+              console.info('documentViewPicker.select to file succeed and uri is:' + uri);
+              if (event) {
+                event.result.handleFileList([uri]);
+              }
+            }).catch((err: BusinessError) => {
+              if (event) {
+                event.result.handleFileList([])
+              }
+              ToolsHelper.showMessage(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`)
+              console.error(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
+            })
+            return true
+          })
+        Column() {
+          Text(this.errorInfo)
+          Button('点击重试')
+            .onClick(() => {
+              this.controller.refresh()
+              this.errorInfo = null
+              this.progress = 0
+            })
+            .margin({ top: 30 })
+        }
+        .visibility(this.errorInfo == null ? Visibility.None : Visibility.Visible)
+        .width('100%')
+        .height('100%')
+        .justifyContent(FlexAlign.Center)
+        .padding({ bottom: 80 })
+
+        XDialogList({
+          // 控制器
+          controller: this.dialogController,
+          // 标题(可选)
+          title: '选择您的操作',
+          // 选择内容列表
+          values: ['刷新', '浏览器打开', '复制地址'],
+          // 用户选择事件
+          onSelected: (index: number, value: string) => {
+            if (index === 0) {
+              this.controller.refresh()
+            } else if (index === 1) {
+              GlobalContext.getContext().openLink(this.controller.getUrl())
+            } else {
+              pasteboard.getSystemPasteboard()
+                .setData(pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.controller.getUrl()), () => {
+                  ToolsHelper.showMessage('已复制到剪切板')
+                })
+            }
+          },
+          // 用户取消事件
+          onCancel: () => {
+            // ToolsHelper.showMessage('用户取消操作')
+          },
+          // 是否可取消(点击空白处,或者物理返回键)
+          autoCancel: true
         })
+
+      }.width('100%').height('100%')
+      .padding({
+        top: WindowHelper.topRectHeight,
+        bottom: WindowHelper.bottomRectHeight
+      })
+
       Column() {
-        Text(this.errorInfo)
-        Button('点击重试')
-          .onClick(() => {
-            this.controller.refresh()
-            this.errorInfo = null
-            this.progress = 0
-          })
-          .margin({ top: 30 })
+        LoadingProgress()
+          .color(Color.White)
+          .width(80).height(80)
+        Text(`努力下载中(${Math.floor(this.receivedSize / this.totalSize * 100)}%)..`)
+          .fontSize(16)
+          .fontColor(Color.White)
       }
-      .visibility(this.errorInfo == null ? Visibility.None : Visibility.Visible)
+      .visibility(this.isDownload ? Visibility.Visible : Visibility.None)
       .width('100%')
       .height('100%')
+      .backgroundColor('#40000000')
       .justifyContent(FlexAlign.Center)
-      .padding({ bottom: 80 })
-
-      XDialogList({
-        // 控制器
-        controller: this.dialogController,
-        // 标题(可选)
-        title: '选择您的操作',
-        // 选择内容列表
-        values: ['刷新', '浏览器打开', '复制地址'],
-        // 用户选择事件
-        onSelected: (index: number, value: string) => {
-          if (index === 0) {
-            this.controller.refresh()
-          } else if (index === 1) {
-            GlobalContext.getContext().openLink(this.controller.getUrl())
-          } else {
-            pasteboard.getSystemPasteboard()
-              .setData(pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.controller.getUrl()), () => {
-                ToolsHelper.showMessage('已复制到剪切板')
-              })
-          }
-        },
-        // 用户取消事件
-        onCancel: () => {
-          // ToolsHelper.showMessage('用户取消操作')
-        },
-        // 是否可取消(点击空白处,或者物理返回键)
-        autoCancel: true
-      })
-
-    }.width('100%').height('100%')
-    .padding({
-      top: WindowHelper.topRectHeight,
-      bottom: WindowHelper.bottomRectHeight
-    })
+    }
+    .align(Alignment.Top)
+    .width('100%')
+    .height('100%')
   }
 }