React Native for OpenHarmony 实战:三方库 react-native-countdown-component 的鸿蒙化适配指南
本文记录把 react-native-countdown-component(倒计时组件)适配到 HarmonyOS 的完整过程。
它是一个单文件的倒计时:显示 天:时:分:秒,每秒递减一次,可以自定义显示哪几段、配色和尺寸。它同时提供了一个"应用回到前台时把后台流失的时间补扣掉"的逻辑。
这轮的价值在于两个都能算出来的量,以及一个真 bug:
① 数字格的尺寸有公式,实测精确吻合
renderDigit = (d) => (
<View style={[styles.digitCont, {width: size * 2.3, height: size * 2.6}, digitStyle]}>
<Text style={[styles.digitTxt, {fontSize: size}, digitTxtStyle]}>{d}</Text>
</View>
);
用 size={30}(density 3)⇒ 预言 69 × 78 vp = 207 × 234 px;实测正好 207 × 234 px ✓
② 递减量可以和真实时间对上
同一实例间隔两次读它的文本:
第 1 次:00 | Days | 01 | Hours | 00 | Minutes | 22 | Seconds
第 2 次:00 | Days | 01 | Hours | 00 | Minutes | 15 | Seconds
两次读取的间隔 = 我的等待 3 秒 + 两次 dump 自身耗时 ≈ 7 秒,而秒数正好 22 → 15(差 7) ✓
⇒ 递减量与真实流逝时间一致,不是"看起来在动"。
③ ★★ 一个真 bug:卸载时会抛错
componentWillUnmount() {
clearInterval(this.timer);
AppState.removeEventListener('change', this._handleAppStateChange); // ⚠️ 这个 API 已经没了
}
实测:typeof AppState.removeEventListener === 'undefined'(而 AppState.addEventListener 仍然存在)⇒ 该 API 在 RN 0.65+ 已被移除 ⇒ 组件被卸载时会抛错。
本轮用"直接查询该方法是否存在"来证实,故意不触发崩溃(触发会污染整页)。
设备侧断言 30 / 30 全部通过。

一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 2.7.1;license: MIT;上游 star 277 |
| 库类型 | 纯 JS 单文件组件库(index.js,275 行):倒计时 |
| 是否需要原生适配 | 不需要(无 harmony/、无 NativeModules) |
| 运行时依赖 | 三个:lodash / sprintf-js / prop-types ⇒ 本机宿主三个都已存在 ⇒ 无需补装(本轮没踩依赖坑) |
prepare / postinstall | ✅ 都没有 |
main | ⚠️ 交付包与 npm 包都没有 main 字段 ⇒ 靠 Node 默认的 index.js(交付包里确实有 index.js) |
| 导出形态 | 两个:export default CountDown + export { CountDown }(同一个 class) |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| 设备侧断言 | ✅ 30 / 30 通过(A 22 + B 5 + C 3) |
| ★★ 数字格尺寸(实测) | size*2.3 × size*2.6 ⇒ size=30 预言 207×234px,实测正好 207×234px |
| ★★ 递减量(实测) | 两次读取间隔 ≈7s,秒数 22 → 15(差 7)⇒ 与真实时间一致 |
| ★★ 真 bug | componentWillUnmount 调用 AppState.removeEventListener(RN 0.65+ 已移除) ⇒ 卸载会抛错 |
| ★ 回调实测 | until=3 的实例:onChange 4 次、onFinish 1 次(到点后触发)✓ |
| ★ 结构 | 4 个原型方法 + 8 个箭头函数类字段(后者 0 个在 prototype 上) |
| ★ propTypes 缺口 | 代码里用了 this.props.running,但 running 不在 propTypes 里 |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules / requireNativeComponent | 全无 |
| ④ | import 图体检 | lodash / sprintf-js / prop-types + react / react-native ⇒ 三个真依赖(都已存在) |
| ⑤ | scripts 扫 prepare 家族 | ✅ 没有 |
npm install "git+https://atomgit.com/oh-react-native/react-native-countdown-component.git#2.7.1-ohos-1.0.0" --ignore-scripts
# 它依赖 lodash / sprintf-js / prop-types(若宿主缺则补装)
npm install lodash sprintf-js prop-types --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-countdown-component') ],
import CountdownTestApp from './CountdownTestApp';
AppRegistry.registerComponent('CountdownTestApp', () => CountdownTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CountdownTestApp
不需要 HAR、权限、ohpm、link-harmony。
三、读源码取到的真值(index.js,275 行)
3.1 结构与两个导出
class CountDown extends React.Component { … }
export default CountDown;
export { CountDown }; // ★ 具名导出与默认导出是同一个 class
实测:命名空间正好 2 个 键(CountDown 与 default),且 CountDown === default ✓
3.2 ★ 成员分两类(4 + 8)
class CountDown extends React.Component {
// ① 原型方法(4 个)
componentDidMount() { … }
componentDidUpdate(prevProps, prevState) { … }
componentWillUnmount() { … }
render() { … }
// ② 箭头函数类字段(8 个)⇒ 挂在实例上,不在 prototype 上
_handleAppStateChange = currentAppState => { … }
getTimeLeft = () => { … }
updateTimer = () => { … }
renderDigit = (d) => { … }
renderLabel = label => { … }
renderDoubleDigits = (label, digits) => { … }
renderSeparator = () => { … }
renderCountDown = () => { … }
}
实测:4 个原型方法全部命中;8 个箭头类字段在 prototype 上一个都没有(0/8),而在实例上8 个都在 ✓
3.3 propTypes(12)与 defaultProps(10)
CountDown.propTypes = { id, digitStyle, digitTxtStyle, timeLabelStyle, separatorStyle,
timeToShow, showSeparator, size, until, onChange, onPress, onFinish }; // 12 键
CountDown.defaultProps = {
digitStyle: {backgroundColor: '#FAB913'}, // ★ 数字格默认黄色
digitTxtStyle: {color: '#000'},
timeLabelStyle: {color: '#000'},
timeLabels: {d: 'Days', h: 'Hours', m: 'Minutes', s: 'Seconds'},
separatorStyle: {color: '#000'},
timeToShow: ['D', 'H', 'M', 'S'],
showSeparator: false,
until: 0,
size: 15,
running: true, // ★ 代码在用,但【不在 propTypes 里】
};
⇒ running 是"未声明的 prop"(传错类型不会有任何提示)✓
3.4 ★★ 尺寸与渲染
renderDigit = (d) => (
<View style={[styles.digitCont, {width: size * 2.3, height: size * 2.6}, digitStyle]}>
<Text style={[styles.digitTxt, {fontSize: size}, digitTxtStyle]}>{d}</Text>
</View>
);
renderLabel = label => ( // 标签字号 = size / 1.8
<Text style={[styles.timeTxt, {fontSize: size / 1.8}, timeLabelStyle]}>{label}</Text>
);
renderSeparator = () => ( // 分隔符字号 = size * 1.2
<Text style={[styles.separatorTxt, {fontSize: size * 1.2}, separatorStyle]}>:</Text>
);
renderCountDown = () => {
const {days, hours, minutes, seconds} = this.getTimeLeft();
const newTime = sprintf('%02d:%02d:%02d:%02d', days, hours, minutes, seconds).split(':');
const Component = this.props.onPress ? TouchableOpacity : View; // ★ 给了 onPress 才是可点容器
return (
<Component style={styles.timeCont} onPress={this.props.onPress}>
{timeToShow.includes('D') ? this.renderDoubleDigits(timeLabels.d, newTime[0]) : null}
{showSeparator && timeToShow.includes('D') && timeToShow.includes('H') ? this.renderSeparator() : null}
… H / M / S 同理 …
</Component>
);
};
⇒ timeToShow 决定显示哪几段;showSeparator 只在相邻两段都存在时才插入 : ✓
⇒ 数字格尺寸 = size*2.3 × size*2.6(可精确预言像素)✓✓
3.5 计时逻辑
constructor(props) { super(props); this.timer = setInterval(this.updateTimer, 1000); }
updateTimer = () => {
if (this.state.lastUntil === this.state.until || !this.props.running) { return; } // ★ running=false 直接不动
if (this.state.until === 1 || (this.state.until === 0 && this.state.lastUntil !== 1)) {
this.props.onFinish?.(); // 到点
this.props.onChange?.(this.state.until);
}
if (this.state.until === 0) { this.setState({lastUntil: 0, until: 0}); }
else {
this.props.onChange?.(this.state.until);
this.setState({lastUntil: this.state.until, until: Math.max(0, this.state.until - 1)});
}
};
3.6 ★★ 卸载路径用的是已被移除的 API
componentDidMount() {
AppState.addEventListener('change', this._handleAppStateChange);
}
componentWillUnmount() {
clearInterval(this.timer);
AppState.removeEventListener('change', this._handleAppStateChange); // ⚠️ RN 0.65+ 已移除
}
实测(不需要真的卸载去触发它):
★ AppState.removeEventListener 的 typeof = undefined
AppState 现有方法 = _emitter, currentState, isAvailable
(而 typeof AppState.addEventListener = 'function' ⇒ 只有【旧 API】被移除)
⇒ 旧 API 没了,新 API 在 ⇒ 这个组件的卸载路径必然抛错 ✓(本轮故意不触发)
四、验证设计
| 判据 | 内容 |
|---|---|
| ① 尺寸(定量) | 数字格 = size*2.3 × size*2.6 ⇒ size=30 时应为 207×234 px |
| ② 递减(定量计时) | 间隔一段时间读两次文本 ⇒ 秒数差值应≈ 真实流逝时间 |
| ③ 回调 | onChange 每秒一次;until=3 时 onFinish 一次 |
| ④ 老 API 存在性 | 直接查 typeof AppState.removeEventListener(不触发崩溃) |
| ⑤ 结构 | 4 原型方法 + 8 箭头类字段(后者不在 prototype 上) |
五、实测结果
5.1 断言 30 / 30
| 组 | 内容 | 结果 |
|---|---|---|
| A 契约 | class + prototype.render、结构(4 原型 + 8 类字段)、2 个导出且同一个 class、propTypes 12 键 / defaultProps 10 键 + 关键默认值、running 不在 propTypes、AppState.removeEventListener 不存在、三个依赖可加载 | 22/22 |
| B 渲染与几何 | 5 个实例渲染未抛错、标题不与状态栏重合、实例上 8 个箭头类字段都存在、读实例 state | 5/5 |
| C 时序与边界 | onChange 次数、onFinish 次数、两条边界记录 | 3/3 |
| 合计 | 30/30 |
关键观测值:
命名空间导出 = CountDown, default
propTypes 键数 = 12 defaultProps 键数 = 10
原型方法命中数(源码 4 个) = 4
8 个箭头类字段里【在 prototype 上】的个数 = 0
★ 实例上存在的箭头类字段个数(源码 8 个) = 8
★ 实例上可读到的 state = {"until":3647,"lastUntil":3648} ← 递减中
★ AppState.removeEventListener 的 typeof = undefined
AppState 现有方法 = _emitter, currentState, isAvailable
lodash 版本 = 4.18.1 sprintf-js 版本 = 1.0.3
★ onChange 次数 = 4 ★ onFinish 次数 = 1
UI 安全区:TITLE measureInWindow y=420 > 117 ✓
5.2 ★★ 数字格尺寸(定量)
size={30}、density 3 ⇒ 预言 30×2.3 = 69vp、30×2.6 = 78vp ⇒ 207 × 234 px。
布局 dump 实测(D1 里连续 4 个数字格中的第一个):
y=1523..1757 (h=234) x=228..435 (w=207) ← ★ 数字格
y=1587..1692 (h=105) x=277..385 (w=108) ← 格内数字文本(如 "00")
y=1763..1805 (h=42) x=291..372 (w=81) ← 格下方标签(如 "Days")
⇒ 207 × 234 px,与公式精确一致 ✓✓
5.3 ★★ 递减量(定量计时)
D1 区(until=3661,即 01:01:01)两次读取、间隔约 7 秒:
第 1 次:00 | Days | 01 | Hours | 00 | Minutes | 22 | Seconds
第 2 次:00 | Days | 01 | Hours | 00 | Minutes | 15 | Seconds
⇒ 秒数 22 → 15,差 7;而真实间隔 = 等待 3s + 两次 dump 自身耗时(每次约 1–2s)≈ 7s ✓
⇒ 递减节奏与真实时间一致 ✓✓
5.4 ★ 回调
onChange4 次(每秒一次)onFinish1 次 ——until={3}的实例在约 3 秒后确实触发了 ✓



六、我第 4 次踩同一个坑(这次改了做法)
我在 A 组里断言了 prototype 上有 updateTimer / getTimeLeft / renderCountDown / renderDigit / renderSeparator —— 结果 5 条全挂,因为它们都是箭头函数类字段:
getTimeLeft = () => { … } // ← 挂实例,不在 prototype
updateTimer = () => { … }
renderDigit = (d) => { … }
这已经是同一类错误第 4 次出现(前几次分别在 onCurrentPositionChanged、toggleItem、getDatesInWeek 上)。
这次改成了不会错的做法:先从源码把成员按写法分成两组,再断言这个"分组"本身:
原型方法命中数(源码 4 个) = 4
8 个箭头类字段里【在 prototype 上】的个数 = 0
★ 实例上存在的箭头类字段个数(源码 8 个) = 8
⇒ 这样即使某个成员改名,也能立刻看出是"分组变了"还是"我认错了" ✓
规矩(写下来提醒自己):断言任何成员之前,先在源码里确认它是
m() {}还是f = () => {};前者在prototype,后者在实例。 与其猜,不如先列出来。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| 卸载会抛错 | componentWillUnmount 调用已被移除的 AppState.removeEventListener(RN 0.65+)⇒ 请自行在业务侧避免直接卸载,或打补丁 |
| 三个运行时依赖 | lodash / sprintf-js / prop-types ⇒ 目录接入要确认宿主已有 |
running 不在 propTypes | 传错没有提示 |
until 只取初始值 | 之后由内部 setInterval 每秒递减;通过 until/id 变化可以重置 |
size 同时控制多处字号 | 数字格 size*2.3 × size*2.6、数字字号 size、标签 size/1.8、分隔符 size*1.2 |
showSeparator 只在相邻两段都存在时才显示 : | 默认 false |
给了 onPress 才会变成 TouchableOpacity | 否则容器是普通 View |
main 字段缺失 | 交付包与 npm 包都靠默认 index.js |
7.2 本次验证的边界
- 卸载路径的抛错未运行时复现(故意不触发,改用"API 存在性"证实);
AppState的前后台补扣逻辑(_handleAppStateChange)未验证(需要真的把应用切后台再回前台);componentDidUpdate里"until/id变化即重置"未验证(没有动态改until);onPress的实际点击未验证(只验证了"给了onPress⇒ 容器是TouchableOpacity"这一层与源码一致);timeLabels自定义、digitTxtStyle/timeLabelStyle/separatorStyle的视觉未逐项像素核对;timeToShow只验证了['M','S']与默认全四段两种组合;idprop 的行为未验证;- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。
未改动库代码。
八、常见问题
Q1:怎么用?
import CountDown from 'react-native-countdown-component';
<CountDown
until={60 * 60 * 2} // 秒
size={24}
digitStyle={{backgroundColor: '#FAB913'}}
digitTxtStyle={{color: '#000'}}
timeLabelStyle={{color: '#000'}}
timeToShow={['H', 'M', 'S']}
showSeparator
onFinish={() => console.log('done')}
onChange={(u) => console.log(u)}
/>
Q2:数字格多大?
width = size*2.3、height = size*2.6(vp)。size=30 时实测 207×234 px(density 3 ⇒ 69×78vp)。
Q3:为什么我的组件一卸载就报 AppState.removeEventListener is not a function?
因为源码在 componentWillUnmount 里调用了这个已经被移除的 API(RN 0.65+)。实测:
typeof AppState.removeEventListener === 'undefined' // 已经没了
typeof AppState.addEventListener === 'function' // 新 API 还在
规避:在业务侧包一层,卸载前先别让这个组件真的 unmount(例如常驻挂载),或者用补丁方式处理(本库源码未改动)。
Q4:想只显示"分:秒"怎么办?
<CountDown timeToShow={['M', 'S']} showSeparator until={125} />
⇒ showSeparator 只在相邻两段都存在时才插入 :。
Q5:running={false} 有什么用?
暂停。updateTimer 里 if (… || !this.props.running) return; ⇒ 完全不动(但 setInterval 仍在跑)。⚠️ 它不在 propTypes 里。
Q6:倒计时能在后台继续走吗?
组件监听 AppState 变化:切到后台时记录时间,回到前台时按"流失的秒数"补扣。⚠️ 但监听器的移除用的是已废弃 API(见 Q3),所以这条路径本身有风险。
Q7:until 改了会怎样?
componentDidUpdate 里比较 until / id,变化时用新的 until 重置。⚠️ 本轮未验证这条。
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。 它是纯 JS、无原生模块、不需要 HAR / 权限 / ohpm。要注意的是三个运行时依赖与卸载路径的老 API。
小结
react-native-countdown-component 是纯 JS 的单文件倒计时组件(275 行)。设备侧 30 / 30 断言通过。
最值得记下来的是这几条:
-
★★ 尺寸与时间是"可算的量",都要拿去和实测对。
尺寸:size*2.3 × size*2.6⇒size=30预言 207×234px,实测正好 207×234px;
时间:两次读取间隔 ≈7s,秒数 22 → 15(差 7) ⇒ 递减节奏与真实时间一致。
⇒ 这两条都比"看起来在倒数/方块大小差不多"硬得多。 -
★★ 老库里可能藏着"已经被移除的 API",而且能用"存在性检查"安全地证实。
componentWillUnmount里调用AppState.removeEventListener(RN 0.65+ 已移除)⇒ 卸载必抛错。
我没有真的去卸载触发它(会污染整页),而是直接查typeof AppState.removeEventListener === 'undefined',并顺带确认新 APIaddEventListener仍在 ⇒ 结论明确、代价为零。 -
★ 结构断言要按"源码写法"来分组,不要凭印象。
这个库有 4 个原型方法 + 8 个箭头函数类字段。我一开始按"都在 prototype 上"断言 ⇒ 挂了 5 条;改成"分组本身作为断言"(4/4 命中、8 个类字段 0 个在 prototype、且 8 个都在实例上)之后一次通过。
⇒ 同一件事已经栽了 4 次,所以把规矩明确写下来:断言成员前先看源码里是m() {}还是f = () => {}。 -
★ 三个依赖都在宿主里 ⇒ 预检阶段就能确定"不用补装"。 这条经验的价值在于:"依赖要不要补装"是可以在动手之前就回答的问题 —— 预检第 ④ 步(import 图)+ 宿主依赖预查已经连续多次提前给出答案。
-
顺带发现的包装问题:交付包与 npm 包都没有
main字段,靠 Node/Metro 默认的index.js兜底(能跑,但不规范);spec.json仍缺upstreamCommit。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-countdown-component(上游 2.7.1 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-countdown-component |
| 适配 TAG | 2.7.1-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 纯 JS 单文件组件库:倒计时(天:时:分:秒) |
| 运行时依赖 | lodash / sprintf-js / prop-types |
prepare / postinstall | ✅ 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/talalmajali/react-native-countdown-component(★277) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = CountdownTestApp) |
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-countdown-component.git#2.7.1-ohos-1.0.0" --ignore-scripts
# 本地目录接入(三个依赖需已在宿主)
npm install ../react-native-countdown-component --ignore-scripts
npm install lodash sprintf-js prop-types --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-countdown-component') ],
import CountDown from 'react-native-countdown-component';
// ① 基本用法
<CountDown
until={60 * 60 * 2} // 秒
size={24} // 数字格 = size*2.3 × size*2.6
digitStyle={{backgroundColor: '#FAB913'}} // 默认就是黄色
digitTxtStyle={{color: '#000'}}
timeLabelStyle={{color: '#000'}}
timeToShow={['H', 'M', 'S']} // 只显示要的段
showSeparator // 相邻段之间显示 ":"
onFinish={() => {}} // 到点
onChange={(until) => {}} // 每秒
onPress={() => {}} // 给了它 ⇒ 容器变成 TouchableOpacity
/>
// ② 暂停
<CountDown until={99} running={false} /> // ⚠️ running 不在 propTypes 里
// ⚠️ 该组件的 componentWillUnmount 调用了已被移除的 AppState.removeEventListener
// ⇒ 卸载时会抛错(本轮用存在性检查证实,未触发)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CountdownTestApp
判据的复现方式
# ① 尺寸:size*2.3 × size*2.6(vp)⇒ ×density 得像素
# 在 dump 里找数字格节点(size=30 时应为 207×234 px)
devecocli ui layout | Select-String 'Custom#View'
# ② 递减:间隔一段时间读两次文本,比较秒数差与真实时间差
devecocli ui layout | Select-String 'Text#Paragraph' # 读 "…|22|Seconds" → "…|15|Seconds"
# ③ 老 API 存在性(不触发崩溃):
# typeof AppState.removeEventListener === 'undefined'(而 addEventListener 仍在)
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3 |
| Node.js / npm | v24.14.0 / 11.9.0 |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 26(26.0.0.32) |
| 设备 | HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3 |
| 本机状态栏高度 | 117px(页面留白取 48vp) |
| 宿主依赖 | lodash 4.18.1 / sprintf-js 1.0.3 / prop-types 15.8.1(都已存在 ⇒ 无需补装) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 首轮 4 分 22 秒(27/32);修结构断言后 4 分 16 秒 ⇒ 30/30 |
| 验证规模 | 设备侧 30 / 30 断言;尺寸定量 1 组(预言 207×234 ⇒ 实测 207×234 px);递减定量 1 组(≈7s 内 22→15);回调 2 项(onChange 4 次 / onFinish 1 次);结构 2 项(4 原型 + 8 类字段);老 API 存在性 2 项;UI 安全区 1 项 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| React Native 鸿蒙官方社区(CPF-RN) | https://atomgit.com/CPF-RN |
| RNOH 框架仓库 | https://atomgit.com/openharmony-RN/ohos_react_native |
| RNOH 三方库使用文档 | https://atomgit.com/OpenHarmony-RN/usage-docs |
| React Native for OpenHarmony 组织 | https://atomgit.com/oh-react-native |
| RN 三方库鸿蒙适配清单 | https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview |
欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN
React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview
更多推荐



所有评论(0)