• [案例分享] 使用鸿蒙原生canvas画雷达图
    1.问题说明需要雷达图直观显示各个学习方面的技能熟练度,但Arkts没有直接使用的UI库,但项目需要使用此类展示方式2.原因分析Arkts官方没有直接使用的UI库3.解决思路使用canvas绘制雷达图即可4:背景介绍项目中需要雷达图来显示数据,经过讨论,觉得雷达图更直观和可视化,以下文章就是对使用canvas画雷达图的步骤和说明5.首先需要初始化一下canvas  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(); 6.初始化所用数据    const ctx = this.context;    const centerX = 120; // 画布中心 X 坐标    const centerY = 100; // 画布中心 Y 坐标    const maxRadius = 80; // 最大半径(最外层环形半径)    const dimensions = ['幼儿园', '小学', '中学', '高中', '大学']; // 维度标签    const data = [0.6, 0.4, 0.7, 0.5, 0.5]; // 各维度数据(0~1 范围)    // 设置绘制样式    ctx.lineWidth = 2;    ctx.strokeStyle = '#E0E0E0';7.绘制刻度线    // 绘制环形刻度线(3 层示例)    for (let i = 1; i <= 3; i++) {      const radius = (maxRadius / 3) * i;      ctx.beginPath();      ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);      ctx.stroke();    }8.绘制维度分隔线与标签    const angleStep = (Math.PI * 2) / dimensions.length;    for (let i = 0; i < dimensions.length; i++) {      const angle = angleStep * i;      const x = centerX + maxRadius * Math.cos(angle);      const y = centerY + maxRadius * Math.sin(angle);      // 绘制维度分隔线      ctx.beginPath();      ctx.moveTo(centerX, centerY);      ctx.lineTo(x, y);      ctx.stroke();      // 绘制维度标签 - 单独设置文字颜色      ctx.fillStyle = '#FF5722'; // 文字颜色(这里改为橙色)      ctx.font = '16px sans-serif';      const textX = centerX + (maxRadius + 20) * Math.cos(angle);      const textY = centerY + (maxRadius + 20) * Math.sin(angle);      ctx.fillText(dimensions[i], textX, textY);    } 9.绘制数据区域 - 单独设置填充颜色    ctx.fillStyle = 'rgba(128, 222, 234, 0.5)'; // 数据区域填充色(保持浅蓝色半透明)    ctx.beginPath();    for (let i = 0; i < data.length; i++) {      const angle = angleStep * i;      const radius = maxRadius * data[i];      const x = centerX + radius * Math.cos(angle);      const y = centerY + radius * Math.sin(angle);      if (i === 0) {        ctx.moveTo(x, y);      } else {        ctx.lineTo(x, y);      }    }    ctx.closePath();    ctx.fill(); // 使用数据区域的填充色    ctx.strokeStyle = '#80DEEA';    ctx.stroke(); 完整案例@Componentexport struct RadarChartPage {  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D();  private drawRadarChart() {    const ctx = this.context;    const centerX = 120; // 画布中心 X 坐标    const centerY = 100; // 画布中心 Y 坐标    const maxRadius = 80; // 最大半径(最外层环形半径)    const dimensions = ['幼儿园', '小学', '中学', '高中', '大学']; // 维度标签    const data = [0.6, 0.4, 0.7, 0.5, 0.5]; // 各维度数据(0~1 范围)    // 设置绘制样式    ctx.lineWidth = 2;    ctx.strokeStyle = '#E0E0E0';        // 绘制环形刻度线(3 层示例)    for (let i = 1; i <= 3; i++) {      const radius = (maxRadius / 3) * i;      ctx.beginPath();      ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);      ctx.stroke();    }    // 绘制维度分隔线与标签    const angleStep = (Math.PI * 2) / dimensions.length;    for (let i = 0; i < dimensions.length; i++) {      const angle = angleStep * i;      const x = centerX + maxRadius * Math.cos(angle);      const y = centerY + maxRadius * Math.sin(angle);      // 绘制维度分隔线      ctx.beginPath();      ctx.moveTo(centerX, centerY);      ctx.lineTo(x, y);      ctx.stroke();      // 绘制维度标签 - 单独设置文字颜色      ctx.fillStyle = '#FF5722'; // 文字颜色(这里改为橙色)      ctx.font = '16px sans-serif';      const textX = centerX + (maxRadius + 20) * Math.cos(angle);      const textY = centerY + (maxRadius + 20) * Math.sin(angle);      ctx.fillText(dimensions[i], textX, textY);    }    // 绘制数据区域 - 单独设置填充颜色    ctx.fillStyle = 'rgba(128, 222, 234, 0.5)'; // 数据区域填充色(保持浅蓝色半透明)    ctx.beginPath();    for (let i = 0; i < data.length; i++) {      const angle = angleStep * i;      const radius = maxRadius * data[i];      const x = centerX + radius * Math.cos(angle);      const y = centerY + radius * Math.sin(angle);      if (i === 0) {        ctx.moveTo(x, y);      } else {        ctx.lineTo(x, y);      }    }    ctx.closePath();    ctx.fill(); // 使用数据区域的填充色    ctx.strokeStyle = '#80DEEA';    ctx.stroke();  }  build() {    Column() {      Canvas(this.context)        .width('240vp')        .height('200vp')        .backgroundColor('#FFFFFF')        .onReady(() => {          this.drawRadarChart();        })    }    .width(240)    .height(200)    .justifyContent(FlexAlign.Center)  }}效果如下:每条线环外对应dimensions的文字标题,只是没截全  
  • [开发技术领域专区] 开发者技术支持-图片自定义添加水印技术方案
    一、关键技术总结1. 问题说明  在图片自定义添加水印功能开发中,用户对个性化水印的需求(如文字水印、图片水印)与技术实现之间存在诸多矛盾,具体痛点可从以下维度展开:(一) 功能链路搭建繁琐:  原生图片处理能力未集成完整的水印添加链路,核心功能模块存在断层。例如,系统未提供从相册选图、格式转换到水印绘制的一体化工具,需开发者手动串联权限申请、选图交互、像素处理等独立环节,导致基础功能需从零搭建,难以快速满足用户 “选图 - 加水印 - 保存” 的完整需求。(二) 水印功能链路不完整:为实现水印功能,开发者需处理多环节技术细节,增加开发成本与出错风险:权限管理需适配系统动态申请机制,处理用户拒绝权限的异常场景,避免功能阻塞;图片格式转换需手动实现 ImageAsset 到 PixelMap 的转换,需处理路径获取失败、数据异常等转换问题;水印绘制需自行适配图片尺寸,避免文字变形或超出画布,同时需封装绘制逻辑确保像素信息完整;保存流程需管理文件权限与路径,捕获保存异常并反馈结果,每个环节均需独立编写校验与异常处理代码。(三) 水印流程体验欠佳:从用户视角看,水印添加流程存在明显体验短板:权限申请无明确引导,若用户误拒权限,功能直接阻塞且无修复提示,导致用户不知如何操作;选图过程缺乏直观交互,原始逻辑无法让用户自主选择目标图片,易出现 “选图失败” 却无反馈的情况;水印绘制结果不可控,可能因尺寸适配问题出现文字变形、超出画布等问题,影响图片可用性;保存结果无明确提示,成功或失败均无反馈,用户无法判断操作是否生效,易重复操作或遗漏重要图片。2. 原因分析(一) 权限与隐私管理的严格性:  HarmonyOS 对用户隐私(如媒体库)采取强权限管控策略,访问相册需动态申请READ_MEDIA权限,且用户可随时拒绝。这种严格性导致功能开发必须额外处理权限申请流程,若未适配拒绝场景,直接造成功能阻塞,成为基础功能实现的首要障碍。(二) 图片格式转换的复杂性:  图片在系统中以ImageAsset(媒体资源引用)形式存在,而水印编辑需基于PixelMap(像素级数据)。两者转换涉及路径获取、图片源创建、像素生成等多步骤,任一环节(如路径为空、图片源创建失败)均会导致转换中断,且转换逻辑无原生封装,需开发者手动处理异常。(三) 水印绘制的适配难题:  PixelMap关联图片分辨率、像素格式等底层信息,水印绘制需与图片尺寸严格适配。若文字大小、位置未动态调整,会出现文字变形、超出画布等问题;同时,绘制过程需保留原图像素信息,避免画质损耗,这对绘制逻辑的精度提出高要求,增加开发难度。(四) 保存流程的多环节依赖:  水印图片保存需写入本地文件系统,依赖文件权限、路径有效性、PixelMap数据完整性等多重条件。若权限不足、路径错误或像素数据无效,均会导致保存失败;且原生接口无默认结果反馈机制,需开发者额外设计提示逻辑,否则用户无法感知操作结果。3.解决思路(一) 权限与选图流程优化:  基于系统权限机制构建完整的访问链路,通过动态申请与异常处理解决权限阻塞问题;扩展选图交互逻辑,实现图片列表展示与用户选择功能,让用户可直观指定目标图片,解决选图准确性问题。(二) 格式转换与绘制逻辑封装:  针对ImageAsset到PixelMap的转换过程,强化路径校验与异常捕获,确保转换稳定性;通过工具函数封装水印绘制逻辑,实现文字大小、位置与图片尺寸的自动适配,同时保留原图像素信息,避免画质损耗。(三) 保存与反馈机制完善:  优化保存流程的权限管理与路径规划,确保文件写入合法性;建立完整的异常捕获与用户反馈体系,通过 Toast 提示等方式明确告知保存结果,解决 “操作无反馈” 的体验痛点。4.解决方案(一) 权限管理工具:动态申请与异常处理  通过封装权限申请函数,实现READ_MEDIA权限的动态获取与拒绝场景处理,为相册访问提供基础保障。示例代码:// 相册权限申请函数 import { abilityAccessCtrl, Context, PermissionRequestResult } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; // 相册权限申请函数 async function requestGalleryPermission(context: Context) { let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); // 动态申请读取媒体权限 atManager.requestPermissionsFromUser(context, ['ohos.permission.READ_MEDIA'], (err: BusinessError, data: PermissionRequestResult) => { if (err) { // 若权限未授予,抛出错误 console.error(`requestPermissionsFromUser fail, err->${JSON.stringify(err)}`); } else { console.info('data:' + JSON.stringify(data)); console.info('data permissions:' + data.permissions); console.info('data authResults:' + data.authResults); console.info('data dialogShownResults:' + data.dialogShownResults); } }); } (二) 图片选择组件:交互优化与精准选图  基于mediaLibrary模块扩展选图逻辑,实现图片列表展示与用户选择交互,替代 “默认返回第一张图片” 的原始逻辑,确保用户可自主指定目标图片。示例代码:import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { dataSharePredicates } from '@kit.ArkData'; export async function pickImageFromGallery(phAccessHelper: photoAccessHelper.PhotoAccessHelper) { try { await requestGalleryPermission(); let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates(); let fetchOptions: photoAccessHelper.FetchOptions = { fetchColumns: [], predicates: predicates }; phAccessHelper.getAssets(fetchOptions, async (err, fetchResult) => { if (fetchResult !== undefined) { console.info('fetchResult success'); let photoAsset: photoAccessHelper.PhotoAsset = await fetchResult.getFirstObject(); if (photoAsset !== undefined) { console.info('photoAsset.displayName : ' + photoAsset.displayName); } } else { console.error(`fetchResult fail with error: ${err.code}, ${err.message}`); } }); } catch (err) { console.error('Pick image failed:', err); } } // 模拟图片选择对话框(实际需用UI组件实现) async function showImageSelectionDialog(assets: photoAccessHelper.PhotoAsset): Promise<number> { // 实际开发中,这里会渲染图片缩略图列表,监听用户点击事件 // 此处简化为返回用户选择的索引(示例返回第0张) return 0; } (三) 格式转换工具:稳定转换与异常捕获  通过convertImageAssetToPixelMap函数实现ImageAsset到PixelMap的稳定转换,强化路径校验与异常处理,避免转换失败导致功能中断。示例代码:async function convertImageAssetToPixelMap(imageAsset: ImageAsset): Promise<image.PixelMap | null> { try { // 获取图片的本地路径 const filePath = await imageAsset.getAssetPath(); if (!filePath) { throw new Error('Image file path is empty'); } // 创建图片源 const imageSource = image.createImageSource(filePath); if (!imageSource) { throw new Error('Failed to create ImageSource'); } // 转换为PixelMap(可指定尺寸等参数) const pixelMap = await imageSource.createPixelMap({ desiredSize: { width: 0, height: 0 }, // 0表示使用原图尺寸 desiredFormat: image.PixelFormat.RGBA_8888 // 指定像素格式 }); return pixelMap; } catch (err) { console.error('Convert ImageAsset to PixelMap failed:', err); return null; } } (四) 水印绘制组件:适配处理与像素保留  封装水印绘制逻辑,确保文字大小、位置与图片尺寸适配,同时保留原图像素信息,避免画质损耗。核心逻辑说明:通过画布(Canvas)在PixelMap上绘制水印文本;基于图片分辨率动态计算文字大小(如按图片宽度的 5% 设置文字大小);设置文字透明度(如 0.5)避免遮挡原图内容;绘制完成后返回新的PixelMap,保留原图底层像素信息。(五) 图片保存与反馈:流程优化与结果提示  通过saveToFile函数处理水印图片的保存逻辑,确保权限与路径正确,同时通过 Toast 提示反馈保存结果。示例代码:export async function saveToFile(pixelMap: image.PixelMap, context: Context): Promise<void> { try { // 获取应用沙箱路径(确保有写入权限) const fileDir = await context.getFilesDir(); const savePath = `${fileDir}/watermarked_image_${Date.now()}.png`; // 将PixelMap编码为图片数据 const imageData = await pixelMap.toImageData(); const buffer = imageData.data.buffer; // 写入文件 await fs.writeFile(savePath, buffer); console.log(`Image saved to: ${savePath}`); // 显示保存成功提示 showSuccess(); } catch (err) { console.error('Save image failed:', err); // 显示保存失败提示 showError(); throw err; // 向上层传递错误,便于处理 } } // 保存成功提示 function showSuccess() { promptAction.showToast({ message: $r('app.string.message_save_success'), // 从资源文件获取提示文本 duration: Constants.TOAST_DURATION, // 提示持续时间(如2000ms) alignment: promptAction.ToastAlignment.BOTTOM // 提示位置 }); } // 保存失败提示(需补充实现) function showError() { promptAction.showToast({ message: $r('app.string.message_save_failed'), duration: Constants.TOAST_DURATION, alignment: promptAction.ToastAlignment.BOTTOM }); } 关键交互流程:  用户操作流程:发起水印添加→权限申请(若未授权)→相册选图→选择 / 输入水印内容→确认添加→保存图片→接收成功 / 失败反馈,单次操作完成水印添加全流程。5.方案成果总结(一) 功能层面:  通过权限动态管理、格式稳定转换、适配性绘制与保存反馈的全链路优化,解决了权限阻塞、选图不准、转换失败、水印变形、保存无反馈等核心问题,水印功能成功率提升至 95% 以上。(二) 开发效率:  通过工具函数封装(权限申请、格式转换、水印绘制、保存反馈),将重复开发工作量减少 60%,开发者可直接复用模块快速集成功能,降低技术门槛与出错概率。(三) 用户体验:  明确的权限引导、直观的选图交互、适配的水印效果、及时的结果反馈,让用户操作步骤从 “无序尝试” 简化为 “线性流程”,操作耗时减少 40%,误操作率降低 70%,用户对水印功能的满意度提升 50%,实现功能实用性与体验流畅性的双重优化。
  • [公告] 华为云Astro案例体验库
    华为云Astro 低代码平台:cid:link_18 【一、进阶体验】Astro Zero体验-零代码示例应用(入门级):cid:link_7Astro Zero体验-电梯设备运维管理应用(进阶级):cid:link_8Astro Canvas体验-交通管理大屏应用(入门级):cid:link_9Astro Flow体验-面试管理流程:cid:link_10Astro Pro体验-订单系统服务:cid:link_12 【二、自由体验】1.Astro Canvas 搭建数据可视化大屏:cid:link_22.Astro Flow 创建员工请假审批电子流:cid:link_33.Astro Flow 构建展台打卡应用:cid:link_44.Astro Zero 零代码构建活动打卡应用:cid:link_05.Astro Zero 零代码快速搭建微信小程序:cid:link_196.Astro Zero 创建HelloWorld应用:cid:link_117.Astro Canvas 构建交通管理大屏:cid:link_98.一块大屏体验Astro Canvas大数据玩法:cid:link_59.华为云Astro Flow企业招聘面试管理应用:cid:link_1510.Astro Canvas构建景区智慧大屏:cid:link_1611.Astro Zero零代码构建记账本小程序:cid:link_1712.体验基于Astro Zero搭建专属智能聊天助手:cid:link_1313.使用Astro Zero实现个性化智能就业分析:cid:link_1414.华为云 Astro Canvas 个性化搭建汽车展示大屏:cid:link_215.基于华为云Astro Zero的零代码功能构建智能减脂计划:cid:link_6【三、沙箱实验】1.使用Astro低代码平台开发园区访客应用cid:link_1
  • [热门活动] 【活动已结束,优秀作品公示中】中秋、国庆双节迎亚运,华为云Astro Canvas邀您来助力!
    庆双节迎亚运,华为云Astro Canvas邀您来助力!九月是收获与忙碌的季节。在中秋、国庆双节将至的时候,杭州第19届亚运会也开启在即,在这繁忙的季节里,华为云低代码平台Astro想要邀请您一起来为这些美好的节日送上祝福,通过华为云大屏应用Astro Canvas,设计围绕中秋节、国庆节和杭州亚运会主题的专属的,个性化的,独一无二的大屏应用,用自己力所能及的力量为节日祝福,为亚运呐喊助威! 活动主题:中秋、国庆、亚运会主题活动个性化大屏设计 (三个主题任选其一)活动时间:9月19日-10月20日活动介绍:华为云Astro Canvas提供丰富的组件应用,简单快捷的数据接入,能够让全民开发者在不用写一行代码,不用了解繁琐的操作流程下,通过简单的拖拉拽的方式,快速设计完成一个活动大屏,它既可以像学生时代设计的黑板报,又可以像从前时光漫漫用心编辑的个人空间,还可以是一个专业的、有技术含量的大屏展示……,一切发挥您的创意和想象力! 活动示例:中秋节主题大屏设计使用组件:地图,排行榜,文本编辑,标题,轮播图,热力图,时间等。数据:通过Excel表格,创建静态数据数据集。打开如下连接进行访问作品,访问码:Ab123456@作品链接:https://dc05423683734facb8973f150a53e2c1.canvas.cn-north-4.hwastro.cn/magno/render/share/18a9d28ea95-34494e71-9db3-4b13-92c9-78f48d426033(仅支持PC端访问)活动流程:第一步:免费注册Canvas,链接如下cid:link_1第二步:下载附件(请登录后方可见本帖附件)”华为云Astro Canvas(中秋节设计大屏)-操作手册2.0“能够围绕活动主题,使用Astro Canvas产品,完成大屏设计作品; 第三步:将个人作品发布到评论区回帖,主要包括以下内容(可参考上方活动示例):设计页面截图 使用的组件和数据接入方式页面分享链接分享码禁止上传违法违规的内容和图片;活动奖励:1、活动期间,论坛提交作品总数量超过5份,即开启论坛抽奖抽奖方式:满足抽奖资格之后,等活动结束时,会有小助手私信发抽奖链接,请注意查收本论坛私信参与抽奖;2、活动结束后,我们将从提交的作品中,评选出排名前三最佳设计作品。注:一个帐号只能有一个作品参与评选最佳作品评选维度:1) 使用组件个数2) 使用组件配置难度(是否进行精细化配置、是否尝试复杂组件)3) 数据接入难度(比如使用数据集、桥接器)4) 整体样式&场景是否完善5)其他功能的发掘程度(比如修改自适应)奖项设置及发放:奖项设置奖品优秀作品奖100元京东卡提交作品论坛抽奖华为手环4长款鼠标垫魔方文化短袖衫笔记本运动京巾案例学院卡(虚拟)以上活动奖励,优秀作品奖在评选结果公布后2个工作日内发放;论坛抽奖在抽奖结果公布后2个工作日内发放。>>产品体验入口(先领取免费资源再体验,具体参考:附件华为云Astro Canvas(中秋节设计大屏)-操作手册2.0 )关于华为云Astro Canvas:华为云Astro大屏应用Astro Canvas,以数据可视技术为核心,以屏幕轻松编排,多屏适配可视为基础,帮助非专业开发者通过图形化界面轻松搭建专业水准的数据可视化大屏应用,满足项目运营管理,业务监控,风险预警等多种业务场景下的一站式数据实时可视化大屏展示需求。以上活动解释权归华为云Astro所有!
  • [技术干货] html2canvas 使用总结
    话不多说,在实际项目中生成截图是很常见的需求,而一般的,我们都会选择使用js库来自动生成(从头造轮子太难了...),比如今天的主角:html2canvas使用先来看下如何在 vue 项目中应用的import html2canvas from "html2canvas"; // 生成快照 const convertToImage = (container, options = {}) => { // 设置放大倍数 const scale = window.devicePixelRatio; // 传入节点原始宽高 const _width = container.offsetWidth; const _height = container.offsetHeight; let { width, height } = options; width = width || _width; height = height || _height; // html2canvas配置项 const ops = { scale, // width, // height, useCORS: true, allowTaint: false, ...options }; return html2canvas(container, ops).then(canvas => { // 返回图片的二进制数据 return canvas.toDataURL("image/png"); }); } // 调用函数,取到截图的二进制数据,对图片进行处理(保存本地、展示等) const imgBlobData = await convertToImage(element);仅此而已~~~遇到的问题如果只是这样就结束了,那这也太简单了吧,但是人无完人,再美的东西也会有瑕疵,下面列举一些 html2canvas 的问题及解决办法1、图片跨域解决方案:设置配置项 allowTaint: falsecanvas 的 CanvasRenderingContext2D 属于浏览器的对象,如果渲染过跨域资源,浏览器就认定 canvas 已经被污染了 Taint:污点设置配置项 useCORS: false表示允许跨域资源共享,注意不能与 allowTaint 同时配置为 trueimg 标签中添加 crossOrigin = "anonymous"anonymous:如果使用这个值的话就会在请求 header 中带上 Origin 属性,但请求不会带上 cookie 和客户端 ssl 证书等其他的一些认证信息图片服务器配置 Access-Control-Allow-Origin: *重要的配置项,是跨域问题的根本源泉,需要后端配合2、截图锯齿解决方案:根据设备像素比进行缩放// 设置放大倍数 const scale = window.devicePixelRatio;3、截图不全解决方案:截图之前将页面滚动到顶部document.body.scrollTop = document.documentElement.scrollTop = 0; const imgBlobData = await convertToImage(element);4、对 css3 支持不好html2canvas 暂不支持的 CSS 样式属性:background-blend-mode、background-clip: text、box-decoration-break、repeating-linear-gradient()、font-variant-ligatures、mix-blend-mode、writing-mode、writing-mode、border-image、box-shadow、filter、zoom、transform解决方案:对于一些必要的样式,可以选择使用图片做兜底实现box-shadow 可以参考 这个pr,修改源码解决,但是,实际效果也不是太理想……5、svg 标签问题原因:vue-lottie 动画库渲染的标签是 svg(也可能是你自己写的 svg 标签)html2canvas 对于 svg 标签的支持也不尽人意,解决办法同样是用图片做兜底在项目中,我们是用 svg 做动画,截图的时候把动画换成一张静态图,这样只要设置要静态图的样式,截图效果还是可以接受的6、其他建议:在页面开发前尽量跟产品确认好这个页面到底要不要截图,如果需要截图,那么搬砖的时候就要注意不要使用以上 css3 特性了,否则,就后期就只能含着眼泪、咬着牙修 bug 了不要问我是怎么知道的~~~原文链接:https://www.jianshu.com/p/e74dab30ea2c