鸿蒙实战:SharePage 报告分享——雷达图预览与 pasteboard 剪贴板

前言

在这里插入图片描述

图:鸿蒙实战第96篇:SharePage 报告分享——雷达图预览与 pasteboard 剪贴板 运行效果截图(HarmonyOS NEXT)

SharePage 解决了一个常见产品需求:让用户把分析结果分享给朋友。这 351 行代码的核心是 @kit.BasicServicesKit 中的 pasteboard(剪贴板) API——将报告摘要文字写入系统剪贴板,用户可粘贴到任何 App 分享。

本篇拆解:

  1. pasteboard 三步写入流程
  2. 2×2 Grid 海报布局(4 种分享卡片样式选择)
  3. AppStorage 运行时类型安全typeof / instanceof 判断)
  4. HandwritingRadar 大尺寸复用(200px 雷达图)

系列导航:95 BindRelationPage · 97 UnclassifiedArchivePage

相关文档:pasteboard API · Grid 组件 · HandwritingRadar


一、pasteboard 剪贴板分享

1.1 三步写入流程

渲染错误: Mermaid 渲染失败: Parse error on line 2: ...art LR A[用户点击\n"复制分享文字"] --> B[paste ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'

1.2 完整实现代码

import { pasteboard } from '@kit.BasicServicesKit'

private async copyShareText(): Promise<void> {
  const result = AppStorage.get<Record<string, Object>>('analysis_result')
  if (!result) {
    promptAction.showToast({ message: '暂无分析结果' })
    return
  }

  // 构建分享文字
  const shareText = this.buildShareText(result)

  try {
    // ① 创建剪贴板数据对象
    const data = pasteboard.createData(
      pasteboard.MIMETYPE_TEXT_PLAIN,
      shareText
    )

    // ② 获取系统剪贴板实例
    const systemPasteboard = pasteboard.getSystemPasteboard()

    // ③ 写入数据
    await systemPasteboard.setData(data)

    promptAction.showToast({ message: '分享文字已复制到剪贴板' })
  } catch (e) {
    hilog.error(TAG, 'pasteboard 写入失败: %{public}s', JSON.stringify(e))
    promptAction.showToast({ message: '复制失败,请重试' })
  }
}

1.3 构建分享文字

private buildShareText(result: Record<string, Object>): string {
  const type = result['personalityType'] as string ?? '未知'
  const score = result['energyScore'] as number ?? 0
  const keywords = result['keywords'] as string[] ?? []

  return `【鹿鹿笔迹分析结果】
🌿 人格类型:${type}
⚡ 情绪能量:${score}/100
🏷 关键词:${keywords.slice(0, 4).join(' · ')}

通过手写笔迹,了解此刻的自己 ✨
#笔迹心理学 #鹿鹿`
}

pasteboard API 说明MIMETYPE_TEXT_PLAIN 是写入纯文本的标准 MIME 类型,写入后可在所有支持粘贴的 App(微信/备忘录/短信)中粘贴。


二、2×2 Grid 海报样式选择

2.1 Grid 布局配置

Grid() {
  ForEach(this.posterStyles, (style: PosterStyle, index: number) => {
    GridItem() {
      this.PosterCard(style, index)
    }
  })
}
.columnsTemplate('1fr 1fr')   // 2 列等分
.rowsTemplate('1fr 1fr')      // 2 行等分
.columnsGap(12)
.rowsGap(12)
.width('100%')
.height(280)                  // 固定高度,确保 2×2 方形网格

2.2 海报样式定义

interface PosterStyle {
  id: number
  title: string
  bgColor: string
  textColor: string
  emoji: string
}

private readonly posterStyles: PosterStyle[] = [
  { id: 0, title: '暖棕', bgColor: '#F5EDE6', textColor: '#3D352E', emoji: '🍂' },
  { id: 1, title: '灰绿', bgColor: '#E8EEE3', textColor: '#2D3A2A', emoji: '🌿' },
  { id: 2, title: '玫瑰', bgColor: '#F5E8E8', textColor: '#3A2A2A', emoji: '🌸' },
  { id: 3, title: '深夜', bgColor: '#2D2D3A', textColor: '#E8E8F0', emoji: '🌙' }
]

2.3 海报卡片 Builder

@Builder
PosterCard(style: PosterStyle, index: number) {
  Column({ space: 6 }) {
    Text(style.emoji).fontSize(20)
    Text(this.personalityType)
      .fontSize(11)
      .fontWeight(FontWeight.Bold)
      .fontColor(style.textColor)
    Text(`${this.energyScore}/100`)
      .fontSize(16)
      .fontFamily(AppFonts.SERIF)
      .fontColor(style.textColor)
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .borderRadius(AppRadius.CARD)
  .backgroundColor(style.bgColor)
  .border({
    width: this.selectedStyle === index ? 2 : 0,
    color: AppColors.PRIMARY
  })
  .animation({ duration: 200 })
  .onClick(() => { this.selectedStyle = index })
}
样式 背景色 文字色 风格
暖棕 #F5EDE6 #3D352E 白天温暖
灰绿 #E8EEE3 #2D3A2A 自然清新
玫瑰 #F5E8E8 #3A2A2A 少女柔和
深夜 #2D2D3A #E8E8F0 深色夜间

三、AppStorage 运行时类型安全

3.1 问题场景

AppStorage.get<Record<string, Object>>('analysis_result') 返回的值在 ArkTS 严格类型检查下需要手动判断字段类型:

private getStringField(result: Record<string, Object>, key: string): string {
  const val = result[key]
  if (typeof val === 'string') return val      // 字符串直接用
  if (val === null || val === undefined) return ''
  return String(val)                           // 其他类型转字符串
}

private getNumberField(result: Record<string, Object>, key: string): number {
  const val = result[key]
  if (typeof val === 'number') return val
  if (typeof val === 'string') return parseFloat(val) || 0
  return 0
}

private getArrayField(result: Record<string, Object>, key: string): string[] {
  const val = result[key]
  if (val instanceof Array) return val as string[]
  return []
}

为什么需要运行时类型检查? Record<string, Object> 的 value 类型是 Object,ArkTS 编译器无法在编译期确认字段类型。使用 typeof / instanceof 运行时判断是处理 Object 类型字段的标准做法。


四、HandwritingRadar 大尺寸复用

// SharePage 中用 200px 尺寸的雷达图(比 ReportDetailPage 的 160px 更大)
HandwritingRadar({
  radarSize: 200,
  scores: this.radarScores,
  showLabels: true,
  animateOnLoad: true
})
.margin({ top: 16, bottom: 16 })

HandwritingRadar 组件接受 radarSize 参数动态调整画布尺寸,雷达图各元素(网格/数据多边形/标签)自动按比例缩放。


五、页面数据读取

5.1 完整数据读取流程

aboutToAppear(): void {
  const result = AppStorage.get<Record<string, Object>>('analysis_result')
  if (!result) {
    // 无数据:兜底显示默认值
    this.personalityType = '分析中...'
    this.energyScore = 0
    return
  }

  this.personalityType = this.getStringField(result, 'personalityType')
  this.energyScore = this.getNumberField(result, 'energyScore')
  this.keywords = this.getArrayField(result, 'keywords')
  this.description = this.getStringField(result, 'description')

  const radarObj = result['radarScores']
  if (radarObj && typeof radarObj === 'object') {
    this.radarScores = radarObj as Record<string, number>
  }

  // 启动入场动画
  this.startEntranceAnimation()
}

八、注意事项与常见问题

8.1 开发注意事项

在实际开发过程中,需特别注意以下几点:

  • API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
  • 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
  • 生命周期:合理使用 aboutToAppear()aboutToDisappear() 管理资源初始化与释放
  • 状态同步:跨页面数据通过 AppStorage 共享,组件内状态使用 @State / @Prop / @Link 装饰器

8.2 常见错误与解决方案

开发过程中的高频问题汇总:

错误现象 可能原因 解决方案
权限被系统拒绝 未在 module.json5 中静态声明 添加 requestPermissions 权限配置项
页面跳转后白屏 路由路径未在 main_pages.json 注册 检查并补充路由配置文件
UI 状态不刷新 状态变量未添加响应式装饰器 为变量加 @State / @Observed 装饰器
数据库查询返回空 查询条件与存储数据格式不匹配 使用 hilog.debug 打印 SQL 条件排查
相机预览黑屏 运行时相机权限未获取 先调用 requestPermissionsFromUser 申请

总结

SharePage 的 351 行代码展示了鸿蒙应用的"本地分享"最佳实践:

  1. pasteboard 三步法createData → getSystemPasteboard → setData,缺一不可
  2. 2×2 Grid 样式选择器columnsTemplate + rowsTemplate 定义网格,GridItem 填充
  3. AppStorage 类型安全typeof + instanceof 运行时判断,避免类型转换崩溃
  4. HandwritingRadar 尺寸参数radarSize: 200 传参调整,组件内部自动缩放
  5. 分享文字模板:产品化文案 + 话题标签,提升二次传播价值

下一篇预告第97篇 UnclassifiedArchivePage——长按多选与批量归档

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:


七、SharePage 技术实现精要

7.1 分享海报的 Canvas 绘制

private async drawSharePoster(ctx: CanvasRenderingContext2D) {
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;

  // 背景:米色渐变
  const grad = ctx.createLinearGradient(0, 0, 0, h);
  grad.addColorStop(0, '#FAF8F4');
  grad.addColorStop(1, '#F0EBE3');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, w, h);

  // 顶部 Logo 区
  ctx.fillStyle = AppColors.accentMain;
  ctx.font = `bold 24px`;
  ctx.fillText('鹿鹿笔迹', 40, 60);

  // 人格类型
  ctx.fillStyle = AppColors.textPrimary;
  ctx.font = `bold 32px`;
  ctx.fillText(this.report.personalityType, 40, 120);

  // 雷达图(嵌入 Canvas 截图)
  await this.drawRadarInCanvas(ctx, 40, 160, w - 80, 200);

  // 情绪标签
  let x = 40;
  for (const tag of this.report.emotionTags.slice(0, 4)) {
    ctx.fillStyle = '#C4956A30';
    ctx.roundRect(x, 380, tag.length * 14 + 20, 32, 8);
    ctx.fill();
    ctx.fillStyle = AppColors.accentMain;
    ctx.font = '14px';
    ctx.fillText(tag, x + 10, 400);
    x += tag.length * 14 + 30;
  }

  // 底部二维码占位(实际使用 image.createQRImage)
  ctx.fillStyle = AppColors.bgSecondary;
  ctx.fillRect(w - 100, h - 120, 80, 80);
}

7.2 剪贴板分享链路

private async copyShareText() {
  const text = `【鹿鹿·笔迹心理分析】
我的人格类型:${this.report.personalityType}
情绪标签:${this.report.emotionTags.join('、')}
${this.report.aiSummary.slice(0, 50)}...
通过鸿蒙应用"鹿鹿"查看完整报告`;

  const pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
  await pasteboard.getSystemPasteboard().setData(pasteData);
  promptAction.showToast({ message: '已复制分享文字,可粘贴到任意 App' });
}

七、SharePage 完整技术实现

7.1 海报渲染与截图导出

除剪贴板分享外,SharePage 也提供了海报截图功能——将报告卡片渲染为图片保存到相册:

import { image } from '@kit.ImageKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';

async saveAsImage(): Promise<void> {
  // 1. 获取组件截图
  const componentSnapshot = await componentSnapshot.get('shareCard');
  const pixelMap = componentSnapshot.pixelMap;

  // 2. 压缩为 JPEG
  const packOpts: image.PackingOption = { format: 'image/jpeg', quality: 90 };
  const imagePacker = image.createImagePacker();
  const buffer = await imagePacker.packing(pixelMap, packOpts);

  // 3. 写入相册
  const helper = photoAccessHelper.getPhotoAccessHelper(getContext());
  const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
  const file = fileIo.openSync(uri, fileIo.OpenMode.WRITE_ONLY);
  fileIo.writeSync(file.fd, buffer);
  fileIo.closeSync(file.fd);

  promptAction.showToast({ message: '海报已保存到相册 📷' });
}

7.2 分享内容模板设计

优质的分享文案模板是提升传播率的关键。SharePage 采用结构化模板:

buildShareText(): string {
  const { report, partner } = this;
  return `
【鹿鹿·笔迹分析报告】
━━━━━━━━━━━━━━━━
📅 日期:${report.createdAt}
🎭 主人格:${report.dominantTrait}
💡 情绪标签:${report.emotionTags.slice(0, 3).join(' / ')}
${partner ? `💑 与 ${partner.nickname} 的匹配度:${report.matchScore}%` : ''}

✨ AI 解读:
${report.aiSummary.slice(0, 80)}...
━━━━━━━━━━━━━━━━
通过「鹿鹿」App 查看完整报告
  `.trim();
}

7.3 分享链路完整流程

用户点击分享

选择分享方式

文字复制

海报保存

二维码分享

pasteboard.setData

componentSnapshot截图

photoAccessHelper写入相册

生成二维码图片

showToast 反馈

7.4 技术要点汇总

功能模块 API 关键点
剪贴板写入 pasteboard.getSystemPasteboard() ohos.permission.READ_PASTEBOARD
组件截图 componentSnapshot.get(id) 组件需设置 id 属性
相册保存 photoAccessHelper.createAsset ohos.permission.WRITE_IMAGEVIDEO
Toast 反馈 promptAction.showToast 操作完成后立即展示

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐