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

- 实现了文件下载的逻辑,包括进度显示和错误处理
- 新增了下载文件的UI展示,显示下载进度和状态
- 优化了导入模块,增加了request和common模块的导入
- 修复了一些代码格式问题,优化了代码结构
这个提交包含在:
徐勤民 2025-05-22 17:10:06 +08:00
父节点 049d2601f9
当前提交 7b1ce19c39

查看文件

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