更新云信和公益咨询相关代码

This commit is contained in:
XiuYun CHEN
2025-07-11 17:33:40 +08:00
parent 2da6409a0b
commit 879646296d
26 changed files with 863 additions and 70 deletions
@@ -52,9 +52,9 @@ export class NimRepository {
console.debug(`Performance Test im start loginSuccess`)
await this.nim.loginService.login(accountId, token);
console.error('----------- 登录成功 -----------')
router.pushUrl({
url: 'pages/Netease/imTabPage'
});
// router.pushUrl({
// url: 'pages/Netease/imTabPage'
// });
console.debug(`Performance Test im loginSuccess`)
ChatKitClient.init(this.nim, appKey)
@@ -62,6 +62,7 @@ export class NimRepository {
} catch (error) {
console.error('----------- 登录失败 -----------', error)
ChatKitClient.init(this.nim, appKey)
throw error as Error
}
}
@@ -0,0 +1,344 @@
import webview from '@ohos.web.webview';
import { HdNav } from '@itcast/basic';
import router from '@ohos.router';
import { image } from '@kit.ImageKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { promptAction } from '@kit.ArkUI';
import { componentSnapshot } from '@kit.ArkUI';
import { fileIo, fileUri } from '@kit.CoreFileKit';
import { display} from '@kit.ArkUI';
const TAG = 'WebViewSaveImage';
@Entry
@Component
struct WebPageSnapshot {
private controller: webview.WebviewController = new webview.WebviewController();
@State params:RouteParams = router.getParams() as RouteParams;
@State contentWidth: number = 0;
@State contentHeight: number = 0;
@State url: string = this.params.url;
@State title: string = this.params.title;
customUserAgent: string = 'gdxz-expert';
// 网页尺寸
@State h5Width: number = 0;
@State h5Height: number = 0;
// Web组件尺寸
private webWidth: number = 0;
private webHeight: number = 0;
// 当前网页位置
private curXOffset: number = 0;
private curYOffset: number = 0;
// 备份当前网页位置
private xOffsetBefore: number = 0;
private yOffsetBefore: number = 0;
// 截图过程的Web组件覆盖
@State webMaskImage: PixelMap | undefined = undefined;
private webMaskImageZIndex: number = -1;
// 合并后的图片
@State mergedImage: PixelMap | undefined = undefined;
@State snapPopupPosition: Position = { x: 0, y: 0 };
//Web是否已经滚动到底部
@State WebTouchBottom: boolean = false;
// 屏幕尺寸
private displayWidth: number = 0;
private displayHeight: number = 0;
onBackPress(): boolean | void {
if (this.controller.accessStep(-1)) {
this.controller.backward();
return true;
}
return false;
}
build() {
Column() {
HdNav({ title: this.title, showRightIcon: false, hasBorder: true ,isLeftAction:true,leftItemAction:()=>{
if (this.controller.accessBackward()) {
this.controller.backward();
} else {
router.back();
}
}})
Web({
src: this.url,
controller: this.controller
})
.id('webViewShot')
.mixedMode(MixedMode.All)
.overScrollMode(OverScrollMode.ALWAYS)
.domStorageAccess(true)
.onAreaChange((oldValue, newValue) => {
// TODO: 高性能知识点: onAreaChange为高频回调,组件变动时每帧都会调用,避免冗余和耗时操作。
this.webWidth = newValue.width as number;
this.webHeight = newValue.height as number;
})
.onControllerAttached(() => {
let userAgent = this.controller.getUserAgent() + this.customUserAgent;
this.controller.setCustomUserAgent(userAgent);
})
.onOverScroll((event) => {
if (event?.yOffset > 0) {
this.WebTouchBottom = true
} else if (event?.yOffset === 0 && this.WebTouchBottom) {
this.WebTouchBottom = false
}
})
.onScroll((event) => {
this.curXOffset = event.xOffset;
this.curYOffset = event.yOffset;
})
.width('100%')
.layoutWeight(1)// 占据剩余空间
.height('100%')
if (this.title == '随访二维码') {
Row(){
SaveButton({text:SaveDescription.SAVE_IMAGE,buttonType:ButtonType.Normal})
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('rgb(63,199,193)')
.width('100%').height(50)
.onClick( () => {
this.snapShot();
})
}.width('100%').height(56).backgroundColor(Color.White).alignItems(VerticalAlign.Top)
}
}
.height('100%')// 关键:约束父容器高度
}
aboutToAppear(): void {
let a=this.url
// 获取屏幕尺寸
const displayData = display.getDefaultDisplaySync();
this.displayWidth = px2vp(displayData.width);
this.displayHeight = px2vp(displayData.height);
}
async saveWebViewImage() {
try {
// 1. 获取整个网页的长截图
this.controller.webPageSnapshot({
id: "webView",
size: { width: this.contentWidth, height:this.contentHeight } // 确保捕获全尺寸
}, async (error, result) => {
if (error) {
promptAction.showToast({ message: `截图失败: ${error.message}` });
console.error('webPageSnapshot error:', error);
return;
}
if (!result?.imagePixelMap) {
promptAction.showToast({ message: '获取截图数据失败' });
return;
}
// 2. 转为JPEG并写入沙箱
const ctx = getContext(this);
const imagePacker = image.createImagePacker();
const arrayBuffer = await imagePacker.packToData(
result.imagePixelMap,
{ format: 'image/jpeg', quality: 100 } // 平衡质量与大小
);
const sandboxPath = ctx.cacheDir + `/web_fullpage_${Date.now()}.jpg`;
const file = fileIo.openSync(sandboxPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
fileIo.writeSync(file.fd, arrayBuffer);
fileIo.closeSync(file.fd);
// 3. 保存到相册
const helper = photoAccessHelper.getPhotoAccessHelper(ctx);
const sandboxUri = fileUri.getUriFromPath(sandboxPath);
const request = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(ctx, sandboxUri);
await helper.applyChanges(request);
promptAction.showToast({ message: '网页已保存至相册' });
});
} catch (e) {
promptAction.showToast({ message: '保存失败: ' + e.message });
console.error('saveWebViewImage error:', e);
}
}
/**
* 截图函数。
*/
async snapShot() {
// 获取Web页面尺寸
this.getWebSize();
// 截图前的状态初始化
await this.beforeSnapshot();
// TODO: 性能知识点: 使用Canvas离屏绘制在缓冲区拼接截图
const canvasSetting: RenderingContextSettings = new RenderingContextSettings(true);
const offCanvasCtx: OffscreenCanvasRenderingContext2D =
new OffscreenCanvasRenderingContext2D(this.h5Width, this.h5Height, canvasSetting);
// 前置常量
const snipTimes = Math.ceil(this.h5Height / this.webHeight);
const lastTime = snipTimes - 1;
const leftoverHeight = this.h5Height % this.webHeight;
let cropLeftover: image.Region = { x: 0, y: 0, size: { height: 0, width: 0 } }
if (this.WebTouchBottom) {
// 这里要分两种情况,1.滚动到底部时,裁剪应该取最后一张除去重复部分以外的底部
cropLeftover = {
x: 0,
y: vp2px(this.webHeight - leftoverHeight),
size: {
height: vp2px(leftoverHeight),
width: vp2px(this.webWidth)
}
};
} else {
// 2.未滚动到底部时,裁剪应该取最后一张leftoverHeight的上部分
cropLeftover = {
x: 0,
y: 0,
size: {
height: vp2px(leftoverHeight),
width: vp2px(this.webWidth)
}
};
}
// 开始截图
for (let i = 0; i < snipTimes; i++) {
const curSnip = await componentSnapshot.get('webViewShot');
// 最后一次截图需要特殊处理,去除重复部分
if (i === lastTime) {
await curSnip.crop(cropLeftover);
offCanvasCtx.drawImage(curSnip, 0, this.webHeight * i, this.webWidth, leftoverHeight);
} else {
offCanvasCtx.drawImage(curSnip, 0, this.webHeight * i, this.webWidth, this.webHeight);
}
// 继续滚动
this.controller.scrollBy(0, this.webHeight);
// 延时保证滚动完成
await sleep(200);
}
// 截图后的操作
await this.afterSnapshot();
// 获取pixelMap
this.mergedImage = offCanvasCtx.getPixelMap(0, 0, this.h5Width, this.h5Height);
try {
// 2. 转为JPEG并写入沙箱
const ctx = getContext(this);
const imagePacker = image.createImagePacker();
const arrayBuffer = await imagePacker.packToData(
this.mergedImage,
{ format: 'image/jpeg', quality: 100 } // 平衡质量与大小
);
const sandboxPath = ctx.cacheDir + `/web_fullpage_${Date.now()}.jpg`;
const file = fileIo.openSync(sandboxPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
fileIo.writeSync(file.fd, arrayBuffer);
fileIo.closeSync(file.fd);
// 3. 保存到相册
const helper = photoAccessHelper.getPhotoAccessHelper(ctx);
const sandboxUri = fileUri.getUriFromPath(sandboxPath);
const request = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(ctx, sandboxUri);
await helper.applyChanges(request);
promptAction.showToast({ message: '网页已保存至相册' });
// 拼接之后修改可动画变量
// this.afterGeneratorImage();
} catch (e) {
promptAction.showToast({ message: '保存失败: ' + e.message });
console.error('saveWebViewImage error:', e);
}
}
/**
* 截图前获取尺寸
*/
getWebSize() {
const SCRIPT = '[document.documentElement.scrollWidth, document.documentElement.scrollHeight]';
this.controller.runJavaScriptExt(SCRIPT).then((result) => {
try {
switch (result.getType()) {
case webview.JsMessageType.ARRAY:
this.h5Width = (result.getArray() as number[])[0]; // 单位是vp
this.h5Height = (result.getArray() as number[])[1];
break;
default:
break;
}
} catch (e) {
}
});
}
/**
* 截图开始前的操作。
* - 保存网页当前位置,用于恢复状态
* - 截图当前页面作为遮罩层,避免用户察觉组件的滚动,提高用户体验
* - Web页面滚动到顶部,准备开始截图
* - 设置截图后小弹窗的位置,提示用户暂时不要操作,等待截图
* - 开启提示小弹窗
*/
async beforeSnapshot() {
// 保存网页当前位置,用于恢复
this.xOffsetBefore = this.curXOffset;
this.yOffsetBefore = this.curYOffset;
this.h5Height = this.curYOffset + Math.ceil(this.webHeight);
// TODO: 知识点: 使用componentSnapshot.get接口直接获取组件的渲染结果,而不需要将屏幕截图
this.webMaskImage = await componentSnapshot.get('webViewShot');
this.webMaskImageZIndex =2;
this.controller.scrollTo(0, 0);
promptAction.showToast({
message: '正在截图,请勿操作...',
duration: 2000
});
// 延时确保已经滚动到了顶部
await sleep(200);
}
/**
* 截图之后的操作。
* - 恢复web页面到截图之前的位置
* - 取消遮罩层
*/
async afterSnapshot() {
this.controller.scrollTo(this.xOffsetBefore, this.yOffsetBefore);
await sleep(200);
this.webMaskImageZIndex = -1;
this.webMaskImage = undefined;
}
/**
* 生成拼接后图片的操作。用于窗口形成移动的动画。
*/
async afterGeneratorImage() {
// 小窗在屏幕中间短暂停留,避免位置突变,无法形成动画
await sleep(200);
// 修改弹窗位置,形成移动动画
}
}
interface RouteParams {
url:string;
title:string;
}
/**
* 异步延时函数。
* @param ms 延时时长,单位 ms。
* @returns Promise 对象,执行回调。
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -41,6 +41,7 @@
"pages/Netease/InterrogationDetailCompPage",
"pages/Netease/PatientSimplyPage",
"pages/Netease/MyOpinionPage",
"pages/PatientsPage/GroupManagementPage"
"pages/PatientsPage/GroupManagementPage",
"pages/WebView/WebPageSnapshot"
]
}