深度解析 HarmonyOS NEXT:Canvas 图表与数据架构实战

从底层原理到最佳实践,深入剖析鸿蒙原生应用的核心技术

前言

在前两篇入门教程之后,本文将从技术深度角度,解析 HarmonyOS NEXT 应用开发的核心难点:

  1. Canvas 自定义图表:如何高效绑制环形图、柱状图、饼图?
  2. 数据架构设计:单例模式 + 监听器模式的最佳实践
  3. 响应式状态管理:ArkTS 的 @State/@Prop/@Watch 深度剖析
  4. 性能优化:列表懒加载、图表缓存、避免过度渲染

项目地址:D:\harmonyos\project\6.4\5\MyApplication
SDK:HarmonyOS NEXT API 23
核心技术:Canvas、Preferences、响应式状态


一、Canvas 图表绑制原理

1.1 Canvas 初始化机制

HarmonyOS NEXT 的 Canvas 基于 HTML5 Canvas API,但有重要差异:

@Component
struct DonutChart {
  // 关键:必须在组件级别声明 context
  private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D();

  build() {
    Canvas(this.canvasContext)
      .width(220)
      .height(220)
      .onReady(() => {
        // onReady 是 Canvas 初始化完成的唯一可靠时机
        this.drawChart();
      })
  }
}

底层原理

Canvas 组件生命周期:
┌─────────────┐
│ 创建组件实例  │
└─────────────┘
       ↓
┌─────────────┐
│ 初始化 Context │  ← canvasContext = new CanvasRenderingContext2D()
└─────────────┘
       ↓
┌─────────────┐
│ 挂载到 UI 树  │
└─────────────┘
       ↓
┌─────────────┐
│ onReady 触发 │  ← 此时才能安全调用绑制 API
└─────────────┘

常见错误

// ❌ 错误:在 aboutToAppear 中绘制
aboutToAppear(): void {
  this.drawChart();  // Canvas 尚未初始化,会失败
}

// ✅ 正确:使用状态标志
@State isReady: boolean = false;

build() {
  Canvas(this.canvasContext)
    .onReady(() => {
      this.isReady = true;
      this.drawChart();
    })
}

1.2 环形图(Donut Chart)实现

数学原理

环形图本质是扇形 + 内圆遮罩

扇形绑制:
- 起点:(cx, cy)
- 圆弧:从 startAngle 到 endAngle
- 闭合:lineTo(cx, cy)

内圆遮罩:
- 在扇形之上绘制一个白色内圆
- 形成环形视觉效果
核心代码
drawDonutChart(): void {
  const ctx = this.canvasContext;
  const cx = 110, cy = 110;
  const outerRadius = 90;
  const innerRadius = 55;

  // 清空画布
  ctx.clearRect(0, 0, 220, 220);

  // 计算总金额
  const total = this.breakdown.reduce((sum, item) => sum + item.amount, 0);
  if (total === 0) {
    this.drawEmptyChart(ctx, cx, cy);
    return;
  }

  // 绘制扇形
  let startAngle = -Math.PI / 2;  // 从12点钟方向开始
  for (let i = 0; i < this.breakdown.length; i++) {
    const item = this.breakdown[i];
    const sliceAngle = (item.amount / total) * Math.PI * 2;
    const endAngle = startAngle + sliceAngle;

    // 创建扇形路径
    const path = new Path2D();
    path.arc(cx, cy, outerRadius, startAngle, endAngle);
    path.lineTo(cx, cy);
    path.closePath();

    // 填充颜色
    ctx.fillStyle = CHART_COLORS[i % CHART_COLORS.length];
    ctx.fill(path);

    startAngle = endAngle;
  }

  // 绘制内圆(遮罩)
  ctx.beginPath();
  ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2);
  ctx.fillStyle = '#FFFFFF';
  ctx.fill();

  // 绘制中心文字
  ctx.fillStyle = '#333333';
  ctx.font = 'bold 16px';
  ctx.textAlign = 'center';
  ctx.fillText(`¥${total.toFixed(0)}`, cx, cy + 6);
}
Path2D 优化技巧
// ❌ 低效:多次调用 ctx.arc + fill
for (let i = 0; i < data.length; i++) {
  ctx.beginPath();
  ctx.arc(cx, cy, radius, start, end);
  ctx.lineTo(cx, cy);
  ctx.closePath();
  ctx.fill();
}

// ✅ 高效:使用 Path2D 批量路径
for (let i = 0; i < data.length; i++) {
  const path = new Path2D();
  path.arc(cx, cy, radius, start, end);
  path.lineTo(cx, cy);
  ctx.fill(path);  // 更高效
}

1.3 柱状图(Bar Chart)实现

坐标系统
Canvas 坐标系:
(0,0) ────────────────→ x
  │
  │
  │
  ↓ y

柱状图绘制区域:
┌────────────────────────┐
│ paddingTop             │
│  ┌────┐ ┌────┐ ┌────┐ │
│  │    │ │    │ │    │ │  drawHeight
│  └────┘ └────┘ └────┘ │
│  1  2  3  ...  n      │ paddingBottom
└────────────────────────┘
  paddingLeft    paddingRight
动态计算柱宽
drawBarChart(): void {
  const paddingLeft = 40;
  const paddingRight = 10;
  const paddingTop = 10;
  const paddingBottom = 24;

  const drawWidth = this.chartWidth - paddingLeft - paddingRight;
  const drawHeight = this.chartHeight - paddingTop - paddingBottom;

  // 根据数据量动态计算柱宽
  const barWidth = Math.max(3, Math.min(12, (drawWidth / this.data.length) * 0.7));
  const gap = drawWidth / this.data.length;

  // 计算最大值(用于归一化)
  let maxVal = 0;
  for (const item of this.data) {
    maxVal = Math.max(maxVal, item.income, item.expense);
  }

  // 绘制柱状
  for (let i = 0; i < this.data.length; i++) {
    const item = this.data[i];
    const x = paddingLeft + i * gap + (gap - barWidth) / 2;

    // 支出柱(左半边)
    if (item.expense > 0) {
      const barH = (item.expense / maxVal) * drawHeight;
      ctx.fillStyle = '#FF6B6B';
      ctx.fillRect(x, paddingTop + drawHeight - barH, barWidth / 2, barH);
    }

    // 收入柱(右半边)
    if (item.income > 0) {
      const barH = (item.income / maxVal) * drawHeight;
      ctx.fillStyle = '#4ECDC4';
      ctx.fillRect(x + barWidth / 2, paddingTop + drawHeight - barH, barWidth / 2, barH);
    }

    // X轴标签(每5天显示一次)
    if (i % 5 === 0 || i === this.data.length - 1) {
      ctx.fillStyle = '#999999';
      ctx.font = '9px';
      ctx.textAlign = 'center';
      ctx.fillText(String(i + 1), x + barWidth / 2, paddingTop + drawHeight + 16);
    }
  }
}
性能优化:避免重复绘制
@Component
struct BarChart {
  @Prop @Watch('onDataChanged') dailyTotals: DailyTotal[] = [];
  @State cachedData: DailyTotal[] = [];
  @State cachedHash: string = '';

  onDataChanged(): void {
    // 计算数据哈希
    const hash = JSON.stringify(this.dailyTotals);
    if (hash === this.cachedHash) {
      return;  // 数据未变化,不重绘
    }

    this.cachedHash = hash;
    this.cachedData = [...this.dailyTotals];
    this.drawChart();
  }
}

二、数据架构设计

2.1 单例模式(Singleton)

为什么需要单例?
┌─────────────────────────────────────────┐
│              应用进程                     │
├─────────────────────────────────────────┤
│                                         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐
│  │ 首页页面  │  │ 记账页面  │  │ 统计页面  │
│  └──────────┘  └──────────┘  └──────────┘
│       ↓             ↓             ↓
│       └─────────────┼─────────────┘
│                     ↓
│            ┌──────────────┐
│            │ 数据服务单例  │  ← 全局唯一实例
│            └──────────────┘
│                     ↓
│            ┌──────────────┐
│            │ Preferences  │  ← 本地存储
│            └──────────────┘
└─────────────────────────────────────────┘
实现方式
export class FinanceDataService {
  private static instance: FinanceDataService | null = null;

  // 私有构造函数,防止外部实例化
  private constructor() {}

  // 静态获取实例方法
  static getInstance(): FinanceDataService {
    if (FinanceDataService.instance === null) {
      FinanceDataService.instance = new FinanceDataService();
    }
    return FinanceDataService.instance;
  }

  // 业务方法...
}
初始化时机
// entry/src/main/ets/entryability/EntryAbility.ets

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // ✅ 在 Ability 创建时初始化
    FinanceDataService.getInstance().init(this.context);
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      // 此时数据服务已初始化完成
    });
  }
}

2.2 监听器模式(Observer Pattern)

设计意图

实现数据变化自动通知UI更新,解耦数据层和视图层。

type DataChangeCallback = () => void;

export class FinanceDataService {
  private listeners: DataChangeCallback[] = [];

  // 注册监听器
  registerListener(callback: DataChangeCallback): void {
    const idx = this.listeners.indexOf(callback);
    if (idx < 0) {
      this.listeners.push(callback);
    }
  }

  // 注销监听器
  unregisterListener(callback: DataChangeCallback): void {
    const idx = this.listeners.indexOf(callback);
    if (idx >= 0) {
      this.listeners.splice(idx, 1);
    }
  }

  // 通知所有监听器
  private notifyDataChanged(): void {
    for (const cb of this.listeners) {
      cb();
    }
  }

  // 保存交易后触发通知
  private async saveTransactions(): Promise<void> {
    await this.store!.put('transactions', JSON.stringify(this.transactions));
    await this.store!.flush();
    this.notifyDataChanged();  // ← 关键
  }
}
页面中的使用
@Entry
@Component
struct Index {
  private service = FinanceDataService.getInstance();
  private refreshCallback = () => this.refreshData();

  aboutToAppear(): void {
    this.service.registerListener(this.refreshCallback);
  }

  aboutToDisappear(): void {
    this.service.unregisterListener(this.refreshCallback);  // ← 防止内存泄漏
  }

  refreshData(): void {
    // 重新加载数据,UI 自动更新
    this.summary = this.service.getMonthlySummary(this.currentYear, this.currentMonth);
  }
}

生命周期安全

// ❌ 错误:忘记注销监听器
aboutToAppear(): void {
  this.service.registerListener(() => this.refreshData());
}
// 页面销毁后,监听器仍在列表中 → 内存泄漏

// ✅ 正确:使用命名函数,便于注销
private refreshCallback = () => this.refreshData();

aboutToAppear(): void {
  this.service.registerListener(this.refreshCallback);
}

aboutToDisappear(): void {
  this.service.unregisterListener(this.refreshCallback);
}

2.3 Preferences 数据持久化

API 概览
import { preferences } from '@kit.ArkData';

// 获取 Preferences 实例
const store = await preferences.getPreferences(context, 'store_name');

// 存储数据(仅支持 string | number | boolean | Array<string>)
await store.put('key', 'value');
await store.put('number_key', 123);
await store.put('array_key', ['a', 'b', 'c']);

// 读取数据
const value = await store.get('key', 'default_value');

// 持久化到磁盘
await store.flush();

// 删除数据
await store.delete('key');
存储复杂对象
// 存储对象:序列化为 JSON
async saveCategories(): Promise<void> {
  const json = JSON.stringify(this.categories);
  await this.store.put('categories', json);
  await this.store.flush();
}

// 读取对象:反序列化
async loadCategories(): Promise<void> {
  const json = await this.store.get('categories', '') as string;
  if (json) {
    this.categories = JSON.parse(json) as Category[];
  }
}
异步特性注意事项
// ❌ 错误:忘记 await
private async saveTransactions(): Promise<void> {
  this.store.put('transactions', json);  // 未 await
  this.store.flush();  // 未 await
  // 数据可能未及时写入磁盘,应用崩溃会丢失
}

// ✅ 正确:确保异步完成
private async saveTransactions(): Promise<void> {
  await this.store.put('transactions', json);
  await this.store.flush();  // 确保写入磁盘
  this.notifyDataChanged();
}

三、响应式状态管理

3.1 @State:组件内部状态

@Entry
@Component
struct Index {
  @State currentMonth: number = 0;
  @State summary: MonthlySummary = { totalIncome: 0, totalExpense: 0, balance: 0 };

  build() {
    Column() {
      Text(`¥${this.summary.balance.toFixed(2)}`)
        .fontSize(34)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.summary.balance >= 0 ? '#FF6B6B' : '#4ECDC4')
    }
  }
}

响应式机制

状态变化 → 框架检测 → 触发重新渲染 → UI 更新

this.summary.balance = 100
        ↓
框架检测到 @State 变化
        ↓
重新执行 build() 方法
        ↓
Text 组件更新显示

3.2 @Prop:父子组件通信

@Component
struct DonutChart {
  @Prop @Watch('onBreakdownChanged') breakdown: CategoryBreakdown[] = [];

  onBreakdownChanged(): void {
    // 父组件传递的 breakdown 变化时触发
    this.drawChart();
  }
}

@Entry
@Component
struct Index {
  @State expenseBreakdown: CategoryBreakdown[] = [];

  build() {
    Column() {
      DonutChart({ breakdown: this.expenseBreakdown })  // 传递给子组件
    }
  }
}

数据流

Index (父组件)
  │
  │ @State expenseBreakdown = [...]
  │
  └──→ DonutChart (子组件)
         │
         │ @Prop breakdown  ← 单向数据流
         │
         └── 监听变化 → 重绘图表

3.3 @Watch:状态变化监听

@Component
struct DonutChart {
  @Prop @Watch('onBreakdownChanged') breakdown: CategoryBreakdown[] = [];

  onBreakdownChanged(): void {
    if (this.isReady) {
      this.drawChart();  // 数据变化时自动重绘
    }
  }
}

对比传统写法

// ❌ 传统方式:手动监听
@Component
struct DonutChart {
  @Prop breakdown: CategoryBreakdown[] = [];

  aboutToAppear(): void {
    // 无法监听 @Prop 变化
  }
}

// ✅ @Watch 方式:声明式监听
@Component
struct DonutChart {
  @Prop @Watch('onBreakdownChanged') breakdown: CategoryBreakdown[] = [];

  onBreakdownChanged(): void {
    this.drawChart();  // 自动触发
  }
}

3.4 数组/对象的响应式陷阱

@State transactions: Transaction[] = [];

// ❌ 错误:直接修改元素,不会触发更新
this.transactions[0].amount = 100;
this.transactions.push(newTx);

// ✅ 正确:重新赋值整个数组
this.transactions = [...this.transactions, newTx];

// ✅ 正确:修改元素后重新赋值
const newTxs = [...this.transactions];
newTxs[0].amount = 100;
this.transactions = newTxs;

原因:ArkTS 的 @State 检测的是引用变化,而非内容变化。


四、列表性能优化

4.1 ForEach vs LazyForEach

ForEach:全量渲染
List() {
  ForEach(this.transactions, (tx: Transaction) => {
    ListItem() {
      TransactionItem({ transaction: tx })
    }
  }, (tx: Transaction) => String(tx.id))
}

特点

  • 一次性渲染所有列表项
  • 适合数据量小(< 100 条)
  • 简单易用
LazyForEach:懒加载
class TransactionDataSource implements IDataSource {
  private dataArray: Transaction[] = [];
  private listeners: DataChangeListener[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): Transaction {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listeners.push(listener);
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const idx = this.listeners.indexOf(listener);
    if (idx >= 0) {
      this.listeners.splice(idx, 1);
    }
  }
}

@State dataSource: TransactionDataSource = new TransactionDataSource();

List() {
  LazyForEach(this.dataSource, (tx: Transaction) => {
    ListItem() {
      TransactionItem({ transaction: tx })
    }
  }, (tx: Transaction) => String(tx.id))
}

特点

  • 按需渲染可见区域
  • 滚动时动态加载/卸载
  • 适合大数据量(> 100 条)

4.2 key 函数的重要性

// ❌ 错误:返回不稳定的 key
ForEach(items, (item) => {
  ListItem() { /* ... */ }
}, (item) => Math.random().toString())  // 每次渲染都变化

// ✅ 正确:返回唯一且稳定的 key
ForEach(items, (item) => {
  ListItem() { /* ... */ }
}, (item) => String(item.id))  // 稳定的 ID

key 的作用

渲染过程:
1. 对比旧 key 列表和新 key 列表
2. 识别新增、删除、移动的项
3. 只更新有变化的项

如果 key 不稳定:
→ 框架认为所有项都变化
→ 全部重新渲染
→ 性能浪费

4.3 列表项优化

// ❌ 低效:复杂的列表项
ListItem() {
  Column() {
    Image($r('app.media.large_image'))  // 大图
      .width(200)
      .height(200)
    Video({ src: 'video.mp4' })  // 视频
    Web({ src: 'https://...' })   // 网页
  }
}

// ✅ 高效:精简的列表项
ListItem() {
  Row() {
    Text(cat.icon).fontSize(24)
    Column() {
      Text(cat.name).fontSize(15)
      Text(tx.note).fontSize(12).fontColor('#888888')
    }
    Text(`¥${tx.amount.toFixed(2)}`).fontSize(17)
  }
}

五、Canvas 性能优化

5.1 避免频繁重绘

@Component
struct DonutChart {
  @Prop @Watch('onBreakdownChanged') breakdown: CategoryBreakdown[] = [];
  @State lastDrawTime: number = 0;

  onBreakdownChanged(): void {
    // 防抖:300ms 内只绘制一次
    const now = Date.now();
    if (now - this.lastDrawTime < 300) {
      return;
    }
    this.lastDrawTime = now;
    this.drawChart();
  }
}

5.2 缓存计算结果

@Component
struct BarChart {
  @Prop dailyTotals: DailyTotal[] = [];
  private cachedMaxVal: number = 0;
  private cachedDataHash: string = '';

  drawChart(): void {
    const hash = JSON.stringify(this.dailyTotals);
    if (hash === this.cachedDataHash) {
      return;  // 数据未变化
    }

    this.cachedDataHash = hash;
    this.cachedMaxVal = Math.max(...this.dailyTotals.map(d => Math.max(d.income, d.expense)));

    // 绘制...
  }
}

5.3 减少绑定操作

// ❌ 低效:每次循环都设置样式
for (const item of data) {
  ctx.fillStyle = item.color;
  ctx.beginPath();
  ctx.arc(cx, cy, radius, start, end);
  ctx.fill();
}

// ✅ 高效:按颜色分组
const colorGroups = new Map<string, { start: number, end: number }[]>();
// 分组...

for (const [color, arcs] of colorGroups) {
  ctx.fillStyle = color;  // 只设置一次
  for (const arc of arcs) {
    ctx.beginPath();
    ctx.arc(cx, cy, radius, arc.start, arc.end);
    ctx.fill();
  }
}

六、架构设计最佳实践

6.1 分层架构

┌─────────────────────────────────────────┐
│           Presentation Layer            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐
│  │  Pages   │  │Components│  │  Charts  │
│  └──────────┘  └──────────┘  └──────────┘
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│            Business Layer               │
│  ┌──────────────────────────────────┐  │
│  │     FinanceDataService (单例)     │  │
│  └──────────────────────────────────┘  │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│             Data Layer                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐
│  │Preferences│  │  Models  │  │  Utils   │
│  └──────────┘  └──────────┘  └──────────┘
└─────────────────────────────────────────┘

6.2 单一职责原则

// ❌ 错误:一个文件包含所有逻辑
// FinanceManager.ets(5000+ 行)
export class FinanceManager {
  // 数据存储
  // 业务逻辑
  // 图表绘制
  // UI 组件
}

// ✅ 正确:职责分离
// models/FinanceModels.ets(数据结构)
// services/FinanceDataService.ets(业务逻辑)
// components/DonutChart.ets(图表组件)
// pages/Index.ets(页面组件)

6.3 依赖注入

// ❌ 错误:直接依赖具体实现
@Component
struct Index {
  private service = FinanceDataService.getInstance();
}

// ✅ 正确:通过接口解耦(未来扩展)
interface IFinanceService {
  getMonthlySummary(year: number, month: number): MonthlySummary;
  addTransaction(tx: Transaction): Promise<void>;
}

@Component
struct Index {
  private service: IFinanceService = FinanceDataService.getInstance();
}

七、调试技巧

7.1 Canvas 调试

drawChart(): void {
  const ctx = this.canvasContext;

  // 绘制边界框(调试用)
  ctx.strokeStyle = '#FF0000';
  ctx.lineWidth = 1;
  ctx.strokeRect(0, 0, this.chartWidth, this.chartHeight);

  // 绘制中心点(调试用)
  ctx.fillStyle = '#FF0000';
  ctx.beginPath();
  ctx.arc(this.chartWidth / 2, this.chartHeight / 2, 3, 0, Math.PI * 2);
  ctx.fill();

  console.log(`Canvas size: ${this.chartWidth}x${this.chartHeight}`);
}

7.2 数据流追踪

async addTransaction(tx: Transaction): Promise<void> {
  console.log(`[Service] Adding transaction: ${JSON.stringify(tx)}`);
  this.transactions.push(tx);
  await this.saveTransactions();
  console.log(`[Service] Total transactions: ${this.transactions.length}`);
}

refreshData(): void {
  console.log(`[Page] Refreshing data for ${this.currentYear}-${this.currentMonth}`);
  this.summary = this.service.getMonthlySummary(this.currentYear, this.currentMonth);
  console.log(`[Page] Summary: ${JSON.stringify(this.summary)}`);
}

7.3 性能分析

drawChart(): void {
  const startTime = Date.now();

  // ... 绘制逻辑

  const elapsed = Date.now() - startTime;
  console.log(`[Performance] Chart drawn in ${elapsed}ms`);

  if (elapsed > 16) {  // 超过一帧时间(60fps)
    console.warn('[Performance] Chart rendering is slow!');
  }
}

八、总结与展望

在这里插入图片描述

8.1 技术要点回顾

技术点 关键原理 最佳实践
Canvas onReady 时机 使用 isReady 标志
单例模式 全局唯一实例 在 Ability 初始化
监听器 观察者模式 记得注销监听器
@State 引用检测 修改后重新赋值
@Prop 单向数据流 配合 @Watch 监听
LazyForEach 按需渲染 大数据量使用

8.2 性能优化清单

Canvas 优化

  • 使用 onReady 时机
  • 缓存计算结果
  • 防抖重绘频率
  • 减少样式切换

列表优化

  • 使用 LazyForEach(大数据)
  • 提供稳定的 key
  • 简化列表项布局

架构优化

  • 单一职责原则
  • 分层架构设计
  • 依赖注入解耦

8.3 扩展方向

技术深化

  • WebGL 高性能图表
  • 动画效果(Lottie)
  • 手势交互

功能扩展

  • 云端同步
  • 数据导出
  • 智能分析

附录:核心代码结构

MyApplication/
├── AppScope/
│   └── app.json5
├── entry/
│   └── src/main/
│       ├── ets/
│       │   ├── entryability/
│       │   │   └── EntryAbility.ets     # 初始化数据服务
│       │   ├── models/
│       │   │   └── FinanceModels.ets    # 数据结构定义
│       │   ├── services/
│       │   │   └── FinanceDataService.ets  # 单例 + 监听器
│       │   └── pages/
│       │       ├── Index.ets            # 首页 + 环形图
│       │       ├── AddTransactionPage.ets
│       │       ├── TransactionListPage.ets
│       │       ├── StatisticsPage.ets   # 柱状图 + 饼图
│       │       └── CategoryManagePage.ets
│       └── module.json5
└── build-profile.json5

本文深入剖析了 HarmonyOS NEXT 应用开发的核心技术,从 Canvas 绑制原理到数据架构设计,希望能为进阶开发者提供有价值的参考。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐