From 7b1ce19c39699e8da1a428ef20e1c3fd2022468b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=8B=A4=E6=B0=91?= Date: Thu, 22 May 2025 17:10:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(XWebview):=20=E6=B7=BB=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8B=E8=BD=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了文件下载的逻辑,包括进度显示和错误处理 - 新增了下载文件的UI展示,显示下载进度和状态 - 优化了导入模块,增加了request和common模块的导入 - 修复了一些代码格式问题,优化了代码结构 --- src/main/ets/pages/XWebview.ets | 606 ++++++++++++++++++-------------- 1 file changed, 335 insertions(+), 271 deletions(-) diff --git a/src/main/ets/pages/XWebview.ets b/src/main/ets/pages/XWebview.ets index 039e197..2305c12 100644 --- a/src/main/ets/pages/XWebview.ets +++ b/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() - }) + 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) + + 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 }) + 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(() => { - router.back() - }) - }.width(65) - - 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 }) { - 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 }) - - 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) + 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() + }) + } + 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() + } } - }, - 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 - }) - .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) + }) + } + 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) + } + // 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未知错误' } - } else { - ToolsHelper.showAlertDialog({ - title: '警告', - msg: '请传入url或content', - action: { - onClick: () => { - router.back() - } + 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]); } + }).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}`); }) - } - 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]); - } - }).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 }) - 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 }) + 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 + 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 }) - }.width('100%').height('100%') - .padding({ - top: WindowHelper.topRectHeight, - bottom: WindowHelper.bottomRectHeight - }) + Column() { + LoadingProgress() + .color(Color.White) + .width(80).height(80) + Text(`努力下载中(${Math.floor(this.receivedSize / this.totalSize * 100)}%)..`) + .fontSize(16) + .fontColor(Color.White) + } + .visibility(this.isDownload ? Visibility.Visible : Visibility.None) + .width('100%') + .height('100%') + .backgroundColor('#40000000') + .justifyContent(FlexAlign.Center) + } + .align(Alignment.Top) + .width('100%') + .height('100%') } }