HarmonyOS NEXT 实战:从零打造一个「能用的」计算器 App
本文介绍了如何从零开发一个功能完整的HarmonyOS NEXT计算器应用。文章聚焦实战,通过构建计算器演示ArkTS语法和ArkUI声明式开发的核心要点。 项目采用MVC架构,核心分为: CalcEngine.ets - 处理计算逻辑和状态管理 Index.ets - 负责UI界面 计算器引擎设计为独立模块,维护当前数值、运算符、表达式预览等状态,实现四则运算、连续计算、异常处理等功能。文章详细
HarmonyOS NEXT 实战:从零打造一个「能用的」计算器 App
很多开发者第一次接触 HarmonyOS NEXT,往往不知道从何下手。官方文档看了不少,但真到动手写项目的时候,还是一脸懵——ArkTS 的语法跟 TypeScript 哪里不一样?ArkUI 的声明式写法怎么组织代码?业务逻辑和 UI 怎么分离?
这篇文章不讲大道理,只带你从零写一个真正能用的计算器。四则运算、百分数、正负号、连续运算、除零保护,全都安排上。我会把每一步的思考过程、踩过的坑、最终方案完整记录下来,你可以直接照着做。
一、项目概览:我们要做什么
先看最终效果——一个界面简洁、功能完整的计算器:
- 顶部显示区域:表达式预览 + 当前数值
- 底部按钮区域:数字键、运算符键、功能键(AC / +/- / %)
- 支持连续运算(如
3 + 5 × 2 =) - 除零保护,不会崩溃
- 数字过长时自动截断,防止溢出
技术栈:
- HarmonyOS NEXT API 23(SDK 6.1.0)
- ArkTS + ArkUI 声明式开发
- 无第三方依赖,纯原生实现
项目结构:
MyApplication/
├── AppScope/
│ ├── app.json5 # 应用全局配置
│ └── resources/base/element/string.json # 应用级字符串
├── entry/
│ ├── src/main/
│ │ ├── module.json5 # 模块配置
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets # Ability 生命周期
│ │ │ ├── entrybackupability/
│ │ │ │ └── EntryBackupAbility.ets # 备份扩展
│ │ │ ├── model/
│ │ │ │ └── CalcEngine.ets # 计算器引擎(核心逻辑)
│ │ │ └── pages/
│ │ │ └── Index.ets # 计算器主界面
│ │ └── resources/
│ │ ├── base/element/
│ │ │ ├── string.json # 模块字符串资源
│ │ │ ├── color.json # 颜色资源
│ │ │ └── float.json # 尺寸资源
│ │ ├── base/profile/
│ │ │ └── main_pages.json # 页面路由配置
│ │ └── base/media/ # 图标资源
│ └── build-profile.json5
├── build-profile.json5 # 工程构建配置
└── hvigorfile.ts # 构建脚本
核心代码只有两个文件:CalcEngine.ets(业务逻辑)和 Index.ets(UI 界面)。这就是 HarmonyOS NEXT 的 MVC 分层思路——Model 负责数据与计算,View 负责渲染与交互,干净利落。
二、搭建项目:DevEco Studio 里的那些事
2.1 创建项目
打开 DevEco Studio,选择 File → New → Create Project,选择 Empty Ability 模板。
关键配置:
| 配置项 | 值 | 说明 |
|---|---|---|
| Project Name | MyApplication | 项目名 |
| Bundle Name | com.calculator.app | 应用包名 |
| Compatible SDK | 6.1.0(23) | API 23 |
| Module Type | Entry | 主模块 |
| Device Type | Phone | 手机 |
创建完成后,DevEco Studio 会自动生成项目骨架。我们要改的主要是 ets/ 下的代码。
2.2 app.json5 配置
AppScope/app.json5 是应用的全局配置,我们修改了 bundleName 和版本信息:
{
"app": {
"bundleName": "com.calculator.app",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
2.3 module.json5 配置
entry/src/main/module.json5 定义了模块能力,关键部分:
{
"module": {
"name": "entry",
"type": "entry",
"deviceTypes": ["phone"],
"pages": "$profile:main_pages",
"abilities": [{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true,
"skills": [{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}]
}]
}
}
skills 里配置了 ohos.want.action.home,这样我们的应用就能出现在桌面上,用户点击图标即可启动。
2.4 main_pages.json 页面路由
{
"src": ["pages/Index"]
}
目前只有一个页面,后续如果想加历史记录页、设置页,在这里注册路由即可。
三、核心逻辑:CalcEngine 计算引擎
这是整个项目最核心的部分。计算器看似简单,但里面的状态管理比想象中复杂——你得处理"用户按完运算符后按数字是追加还是替换"“连续按运算符怎么处理”"按完等号再按数字如何重置"等一系列边界情况。
3.1 设计思路
我把计算器引擎设计为一个纯逻辑类,不依赖任何 ArkUI 组件。好处显而易见:
- 可独立测试:不需要启动模拟器就能验证计算逻辑
- 关注点分离:UI 只负责展示和转发事件,引擎只负责计算
- 可复用:如果以后做科学计算器,引擎可以扩展而不影响 UI
引擎需要维护以下状态:
private _display: string = '0'; // 当前显示文本
private _previousOperand: number = 0; // 上一个操作数
private _currentOperand: number = 0; // 当前操作数
private _pendingOperator: CalcOperator = CalcOperator.NONE; // 待执行运算符
private _isNewEntry: boolean = false; // 是否正在输入新数字
private _justEvaluated: boolean = false; // 是否刚按了等号
private _expressionPreview: string = ''; // 表达式预览
private _hasError: boolean = false; // 是否发生错误
3.2 运算符枚举
export enum CalcOperator {
NONE = '',
ADD = '+',
SUBTRACT = '−',
MULTIPLY = '×',
DIVIDE = '÷'
}
这里用 Unicode 字符 −(U+2212)和 ×(U+00D7)而不是 ASCII 的 - 和 *,这样在界面上显示更美观,也跟 iOS 计算器保持一致。
3.3 输入数字:inputDigit
这是最基本也是最需要仔细处理的逻辑:
inputDigit(digit: string): void {
if (this._hasError) return;
// 刚算完结果,按数字开始新计算
if (this._justEvaluated) {
this.clearAll();
this._justEvaluated = false;
}
if (this._isNewEntry) {
// 刚按了运算符,显示区替换为新数字
this._display = digit;
this._isNewEntry = false;
} else {
if (digit === '.') {
// 小数点:已有则忽略
if (this._display.includes('.')) return;
this._display += '.';
} else {
if (this._display === '0') {
// 前导零替换
this._display = digit;
} else {
// 限制输入长度,防止溢出
if (this._display.replace('-', '').replace('.', '').length >= 15) return;
this._display += digit;
}
}
}
this._currentOperand = parseFloat(this._display);
}
几个关键点:
- 错误状态下禁止输入:如果除零报错了,必须先按 AC 才能继续操作
_justEvaluated机制:按完=得到结果后,再按数字应该开始全新计算,而不是在结果后面追加- 前导零处理:显示
0时按5,应该变成5而不是05 - 小数点去重:已经有小数点就不能再按
- 长度限制:15 位有效数字,防止浮点数精度问题
3.4 输入运算符:inputOperator
运算符的逻辑最复杂,因为要处理三种场景:
inputOperator(op: CalcOperator): void {
if (this._hasError) return;
if (this._pendingOperator !== CalcOperator.NONE && !this._isNewEntry) {
// 场景1:连续运算 —— 3 + 5 × → 先算 3+5=8,然后等待 × 的下一个数
this.evaluate();
} else if (this._isNewEntry && this._pendingOperator !== CalcOperator.NONE) {
// 场景2:替换运算符 —— 3 + 然后按 × → 变成 3 ×
this._pendingOperator = op;
this._expressionPreview = this.formatOperand(this._previousOperand) + ' ' + op + ' ';
return;
}
// 场景3:首次输入运算符,保存当前数
this._previousOperand = parseFloat(this._display);
this._pendingOperator = op;
this._isNewEntry = true;
this._justEvaluated = false;
this._expressionPreview = this.formatOperand(this._previousOperand) + ' ' + op + ' ';
}
三种场景的通俗解释:
| 场景 | 用户操作 | 引擎行为 |
|---|---|---|
| 连续运算 | 3 + 5 × |
先算 3+5=8,把 8 存为上一操作数,等待输入 × 后面的数 |
| 替换运算符 | 3 + × |
用户改主意了,把 + 换成 ×,不执行计算 |
| 首次运算 | 3 + |
保存 3,等待下一个操作数 |
3.5 执行计算:evaluate
evaluate(): string {
if (this._hasError || this._pendingOperator === CalcOperator.NONE) {
return this._display;
}
this._currentOperand = parseFloat(this._display);
const prev = this._previousOperand;
const curr = this._currentOperand;
let result: number;
switch (this._pendingOperator) {
case CalcOperator.ADD:
result = prev + curr;
break;
case CalcOperator.SUBTRACT:
result = prev - curr;
break;
case CalcOperator.MULTIPLY:
result = prev * curr;
break;
case CalcOperator.DIVIDE:
if (curr === 0) {
// 除零保护
this._hasError = true;
this._display = '错误';
this._expressionPreview = '';
this._pendingOperator = CalcOperator.NONE;
return '错误';
}
result = prev / curr;
break;
default:
return this._display;
}
// 构建完整表达式预览:5 + 3 =
this._expressionPreview =
this.formatOperand(prev) + ' ' + this._pendingOperator + ' ' +
this.formatOperand(curr) + ' =';
this._display = this.formatResult(result);
this._previousOperand = result;
this._pendingOperator = CalcOperator.NONE;
this._isNewEntry = true;
this._justEvaluated = true;
return this._display;
}
除零保护是最关键的安全措施。如果不处理,JavaScript 的 Infinity 或 NaN 会在 UI 上显示一堆奇怪的东西,甚至导致后续逻辑崩溃。这里直接设 _hasError = true,之后所有操作都被阻断,直到用户按 AC。
3.6 辅助功能
引擎还提供了几个辅助功能:
/** 清除所有 */
clearAll(): void {
this._display = '0';
this._previousOperand = 0;
this._currentOperand = 0;
this._pendingOperator = CalcOperator.NONE;
this._isNewEntry = false;
this._justEvaluated = false;
this._expressionPreview = '';
this._hasError = false;
}
/** 正负号切换 */
toggleSign(): void {
if (this._hasError || this._display === '0') return;
this._display = this._display.startsWith('-')
? this._display.substring(1)
: '-' + this._display;
this._currentOperand = parseFloat(this._display);
}
/** 百分数 */
percentage(): void {
if (this._hasError) return;
const val = parseFloat(this._display) / 100;
this._display = this.formatResult(val);
this._currentOperand = parseFloat(this._display);
}
3.7 数字格式化
计算结果需要合理格式化,避免 0.1 + 0.2 = 0.30000000000000004 这种经典浮点问题:
private formatResult(num: number): string {
if (!isFinite(num)) return '错误';
if (Math.abs(num) < 1e-10) return '0';
if (Number.isInteger(num) && Math.abs(num) < 1e15) return num.toString();
if (Math.abs(num) > 1e15) return num.toExponential(6);
return parseFloat(num.toFixed(10)).toString();
}
处理策略:
- 非有限数(
Infinity、NaN)→ 显示"错误" - 极小值(
< 1e-10)→ 显示0 - 整数 → 直接显示
- 超大数 → 科学计数法
- 普通小数 → 保留最多 10 位有效小数
四、UI 界面:ArkUI 声明式布局
有了引擎,接下来是界面。ArkUI 的声明式语法跟 SwiftUI 很像——你描述"界面应该长什么样",框架负责把它渲染出来。
4.1 整体布局结构
@Entry
@Component
struct Index {
@State display: string = '0';
@State expression: string = '';
@State hasError: boolean = false;
private engine: CalcEngine = new CalcEngine();
build() {
Column() {
// 显示区域
Column() { ... }
.layoutWeight(1) // 占据剩余空间
// 分隔线
Divider()
// 按钮区域
Column({ space: 12 }) { ... }
}
.width('100%')
.height('100%')
.backgroundColor('#F8F9FA')
}
}
整体是一个纵向 Column:上面显示区域用 layoutWeight(1) 撑满剩余空间,下面按钮区域按内容高度排列。这样无论屏幕多大,显示区域都会自动伸缩。
4.2 显示区域
Column() {
// 表达式预览(小字,灰色)
Text(this.expression)
.fontSize(16)
.fontColor('#999')
.width('100%')
.textAlign(TextAlign.End)
.margin({ top: 20, bottom: 4 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 主显示(大字,加粗)
Text(this.display)
.fontSize(this.hasError ? 32 : 48)
.fontWeight(FontWeight.Bold)
.fontColor(this.hasError ? '#E74C3C' : '#1a1a2e')
.width('100%')
.textAlign(TextAlign.End)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
.padding({ left: 24, right: 24, top: 40, bottom: 20 })
.layoutWeight(1)
.justifyContent(FlexAlign.End)
两个 Text 组件纵向排列:
- 上面的小字显示表达式预览(如
5 + 3 =),灰色、右对齐、超长省略 - 下面的大字显示当前数值,右对齐、加粗
错误时字体变小(32 → 48)、颜色变红(#E74C3C),给用户明确的视觉反馈。
4.3 按钮区域——@Builder 复用
5 行 4 列的按钮如果每个都手写,代码会非常冗长。我用 @Builder 做了复用:
@Builder
ButtonRow(labels: string[]) {
Row() {
ForEach(labels, (label: string) => {
this.CalcButton(label)
}, (label: string) => label)
}
.width('100%')
.height(64)
.padding({ left: 16, right: 16 })
}
@Builder
CalcButton(label: string) {
Text(label)
.fontSize(26)
.fontColor(this.getBtnTextColor(label))
.fontWeight(this.getBtnFontWeight(label))
.width('calc((100% - 12px * 4) / 4)')
.height(64)
.textAlign(TextAlign.Center)
.backgroundColor(this.getBtnBg(label))
.borderRadius(32)
.margin({ left: 6, right: 6 })
.onClick(() => this.onButtonClick(label))
}
按钮宽度用了 calc() 表达式:(100% - 12px × 4) / 4,即总宽度减去 4 个间隔后均分。ArkUI 的 calc() 支持跟 CSS 一样的计算语法,非常方便。
按钮配色方案:
| 按钮类型 | 背景色 | 文字色 | 示例 |
|---|---|---|---|
| 功能键(AC / +/- / %) | #E8E8E8 浅灰 |
#1a1a2e 深色 |
AC |
| 数字键 | #3a3a4a 深灰 |
#fff 白色 |
7 |
| 运算符键 | #FF6B35 橙色 |
#fff 白色 |
+ |
| 等号键 | #1a1a2e 深色 |
#fff 白色 |
= |
private getBtnBg(label: string): ResourceColor {
if (label === 'AC' || label === '+/-' || label === '%') return '#E8E8E8';
if (label === '÷' || label === '×' || label === '−' || label === '+') return '#FF6B35';
if (label === '=') return '#1a1a2e';
return '#3a3a4a';
}
4.4 第五行按钮的特殊处理
最后一行(0 . =)的 0 键比较宽,需要特殊处理:
Row() {
Text('0')
.fontSize(26).fontColor('#fff')
.width(136).height(64).textAlign(TextAlign.Center)
.backgroundColor('#3a3a4a').borderRadius(32)
.margin({ left: 6, right: 6 })
.onClick(() => this.onButtonClick('0'))
Text('.')
.fontSize(26).fontColor('#fff')
.width(64).height(64).textAlign(TextAlign.Center)
.backgroundColor('#3a3a4a').borderRadius(32)
.margin({ left: 6, right: 6 })
.onClick(() => this.onButtonClick('.'))
Text('=')
.fontSize(26).fontColor('#fff').fontWeight(FontWeight.Bold)
.width(64).height(64).textAlign(TextAlign.Center)
.backgroundColor('#1a1a2e').borderRadius(32)
.margin({ left: 6, right: 6 })
.onClick(() => this.onButtonClick('='))
}
.width('100%')
.height(64)
.padding({ left: 16, right: 16 })
0 键宽度 136px,是普通按钮的两倍左右,模拟真实计算器的宽零键效果。
4.5 事件处理:onButtonClick
所有按钮点击最终汇聚到 onButtonClick:
onButtonClick(label: string): void {
switch (label) {
case 'AC': this.engine.clearAll(); break;
case '+/-': this.engine.toggleSign(); break;
case '%': this.engine.percentage(); break;
case '÷': this.engine.inputOperator(CalcOperator.DIVIDE); break;
case '×': this.engine.inputOperator(CalcOperator.MULTIPLY); break;
case '−': this.engine.inputOperator(CalcOperator.SUBTRACT); break;
case '+': this.engine.inputOperator(CalcOperator.ADD); break;
case '=': this.engine.evaluate(); break;
case '.': this.engine.inputDigit('.'); break;
default: this.engine.inputDigit(label); break;
}
this.refreshDisplay();
}
每次操作后调用 refreshDisplay() 把引擎状态同步到 UI:
private refreshDisplay(): void {
this.display = this.engine.display;
this.expression = this.engine.expressionPreview;
this.hasError = this.engine.hasError;
}
这里体现了 ArkUI 响应式更新的核心:@State 变量一旦被赋新值,UI 自动刷新,不需要手动操作 DOM。
五、Ability 生命周期:EntryAbility
虽然计算器不需要复杂的生命周期管理,但 EntryAbility 仍然是应用的入口,理解它的执行流程很重要:
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 设置颜色模式跟随系统
this.context.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
hilog.info(DOMAIN, 'testTag', 'Ability onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// 加载主页面
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
}
生命周期顺序:onCreate → onWindowStageCreate → 页面渲染 → onForeground → 用户交互 → onBackground → onWindowStageDestroy → onDestroy
onWindowStageCreate 里的 loadContent('pages/Index') 就是加载我们写的 Index.ets 页面。
六、踩坑记录
坑 1:按钮宽度 calc() 表达式的写法
最初我写的是 width('25%'),4 个按钮各 25%,但加上 margin 后总宽度超出屏幕,最后一列被挤出去了。
解决方案:用 calc((100% - 12px * 4) / 4) 精确计算。4 个按钮之间有 4 个 margin 间隔(每个 6px × 2 侧 = 12px),减去后均分。
ArkUI 的 calc() 支持加减乘除和混合单位运算,语法基本兼容 CSS,这是做精确布局的利器。
坑 2:连续运算的顺序问题
计算器不是简单地把两个数算一下就完事了。用户可能这样操作:3 + 5 × 2 =。
在真实的计算器中,这不是先乘后加(那是数学规则),而是从左到右依次计算——这是所有手机计算器的通用行为。所以 3 + 5 × 2 的计算过程是:
3 + 5→ 88 × 2→ 16
而不是数学上的 5 × 2 = 10, 3 + 10 = 13。
在 inputOperator 里,当检测到已有待执行运算符且用户又按了新运算符时,先执行前一个运算,再把结果存为 _previousOperand,等待下一个操作数。
坑 3:0.1 + 0.2 的浮点精度问题
JavaScript/ArkTS 的浮点数遵循 IEEE 754 标准,所以 0.1 + 0.2 = 0.30000000000000004。
在 formatResult 方法中,我用 parseFloat(num.toFixed(10)) 来截断多余的小数位。toFixed(10) 保留 10 位小数,parseFloat 再去掉尾部的零。这样 0.30000000000000004 就变成了 0.3。
坑 4:按完等号再按数字的预期行为
这个场景很容易被忽略:用户按了 3 + 5 = 得到 8,然后按 7,预期是什么?
- 错误行为:显示变成
87(在结果后面追加) - 正确行为:显示变成
7(开始全新计算)
通过 _justEvaluated 标志实现:按 = 后设为 true,下次 inputDigit 检测到它就 clearAll() 重新开始。
坑 5:Text 组件溢出处理
计算结果可能很长(比如 1.2345678901234),不做处理会撑爆布局。
Text(this.display)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
maxLines(1) 限制单行,textOverflow 让超长文本显示省略号。同时引擎内部做了 15 位有效数字的截断,从源头控制长度。
七、构建与运行
7.1 构建配置
build-profile.json5 中的关键配置:
{
"app": {
"products": [{
"name": "default",
"targetSdkVersion": "6.1.0(23)",
"compatibleSdkVersion": "6.1.0(23)",
"runtimeOS": "HarmonyOS"
}]
}
}
targetSdkVersion 和 compatibleSdkVersion 都设为 API 23,确保使用最新的 HarmonyOS NEXT 能力。
7.2 运行
- 连接真机或启动模拟器
- 点击 DevEco Studio 的运行按钮(▶️)
- 等待编译部署完成
截图占位:
八、架构思考:为什么要把引擎分离出来
你可能觉得一个计算器没必要搞 MVC,全写在 Index.ets 里也能跑。但我想说几个实际的好处:
8.1 可测试性
引擎类不依赖任何 ArkUI API,你可以直接在 Node.js 里跑单元测试:
const engine = new CalcEngine();
engine.inputDigit('3');
engine.inputOperator(CalcOperator.ADD);
engine.inputDigit('5');
engine.evaluate();
console.log(engine.display); // "8"
如果逻辑混在 UI 里,你得启动模拟器才能验证,效率天差地别。
8.2 可维护性
想象一下,产品经理说要加"历史记录"功能。如果逻辑和 UI 耦合,你得在按钮点击回调里同时管理 UI 状态和历史记录逻辑,代码很快就会变成面条。
分离后,CalcEngine 暴露一个 history 数组就行,UI 只负责展示。
8.3 可复用性
引擎是纯逻辑,没有 UI 依赖。如果以后要:
- 做一个带侧边栏的平板计算器
- 做一个语音计算器
- 在 Widget 卡片里显示计算结果
引擎代码零改动,只换 UI 层就行。
九、可能的扩展方向
这个计算器是 MVP 版本,还有很多可以加的功能:
- 历史记录:用
@ohos.data.preferences或轻量数据库存储运算历史 - 科学计算:加
sin、cos、log等函数,扩展CalcEngine即可 - 键盘输入:监听物理键盘事件,在
onKeyEvent中处理 - 深色模式:监听系统颜色模式切换,动态修改配色方案
- 横屏布局:检测屏幕方向,横屏时展示科学计算器模式
- 手势操作:左滑退格、长按复制结果等交互优化
- 动画效果:按钮按下缩放、数字切换过渡动画
十、总结
这篇文章从零开始,完整实现了一个 HarmonyOS NEXT 计算器应用。核心要点:
- 项目结构:
model/CalcEngine.ets+pages/Index.ets,清晰的 MVC 分层 - 计算引擎:独立于 UI 的纯逻辑类,处理输入、运算、状态管理、错误保护
- ArkUI 界面:
@Builder复用按钮组件,@State响应式更新,calc()精确布局 - 边界处理:连续运算、运算符替换、除零保护、浮点精度、等号后重置
- 踩坑经验:5 个实战踩坑记录,每个都有原因分析和解决方案
HarmonyOS NEXT 的开发体验说实话还不错——ArkTS 本质上就是带类型系统的 TypeScript,ArkUI 的声明式写法也很直观。主要的学习曲线在两个地方:一是项目结构和配置体系(module.json5、build-profile.json5、各种 json5),二是 ArkUI 的组件 API(有哪些属性可以设、怎么设)。
这两个问题没有捷径,只能多看官方文档、多写代码。但好消息是,一旦上手,开发效率是很高的——尤其是习惯了声明式 UI 之后,再回去写命令式 UI 反而觉得别扭。
作者注:本文基于 HarmonyOS NEXT SDK 6.1.0(23) 编写,API 可能随版本迭代发生变化,请以最新官方文档为准。
更多推荐
所有评论(0)