本文记录把 react-native-confirmation-code-field(验证码 / PIN 输入框)适配到 HarmonyOS 的完整过程。

它是一个很典型的验证码输入组件:画一排格子,用户的输入落进格子里;同时提供了一个闪烁光标、一个掩码符号,以及几个配套 hook(填满自动失焦、点击某格清空到该格)。

它的设计有个特点:格子长什么样完全由使用者决定 —— 组件只负责"把每个格子该显示什么算出来",再回调 renderCell 让你自己渲染:

// 每个格子
renderCell({ index, symbol, isFocused: isFocused && isFirstEmptySymbol })
//                                     ↑ 只对"第一个空格"为真

实测(cellCount={6}、value="12"):

CELL F1 idx=0 sym="1" -        CELL F1 idx=1 sym="2" -
CELL F1 idx=2 sym="" FOCUS     ← ★ 只有它带 FOCUS
CELL F1 idx=3 sym="" -         CELL F1 idx=4 sym="" -        CELL F1 idx=5 sym="" -

⇒ "焦点只落在第一个空格上"这条规则被精确证实 ✓✓(symbol 的切分同时印证了 getSymbols)

另外两个纯函数也可以直接验,不需要渲染:

isLastFilledCell('1234', 2) === true      // 语义是"当前值的倒数第二格"
isLastFilledCell('1234', 3) === false
getSymbols('12', 4) → ['1','2','','']

设备侧断言 29 / 29 全部通过。

在这里插入图片描述

⚠️ 本轮我自己踩了两个 React 的坑(都不在库里,而在我的测试页代码里),其中一个直接把整页变成了红屏 —— 排查过程比结论更值得记,见第六节。


一、先说结论

项结果
上游最新版9.0.0;license: MIT;上游 star 1.2k
库类型纯 JS 组件库:CodeField + Cursor + MaskSymbol + 3 个 hook / 工具函数
是否需要原生适配不需要(无 harmony/、无 NativeModules)
运行时依赖✅ 零(dependencies 为空)
prepare / postinstall✅ 都没有(只有发布时的 prepublishOnly)
mainesm/index.js(tsc 编译产物,交付包不带 src/)
peerDependenciesreact >= 19.0.0、react-native >= 0.78.0 ⇒ 明确面向 React 19 ✓
导出形态正好 6 个具名导出:CodeField / Cursor / MaskSymbol / useClearByFocusCell / useBlurOnFulfill / isLastFilledCell
需要 HAR / 权限 / ohpm❌ 都不需要
设备侧断言✅ 29 / 29 通过(A 22 + B 4 + C 3)
★★ 焦点规则实测cellCount=6 + value="12" ⇒ 只有 index=2(第一个空格)带 FOCUS
★ 纯函数直调isLastFilledCell 6 例、getSymbols 3 例全部符合
★ 隐藏输入框absoluteFill + opacity: 0.015 + maxLength={cellCount} + autoComplete 非 android 取 'one-time-code'
★ 我自己的两个坑① 事件对象读进 setState 的 updater ⇒ 红屏 ② 在 renderCell(渲染期回调)里 setState

二、判定与接入

步做法结果
①package.json 里有没有 harmony.autolinking没有
②仓库里有没有 harmony/没有
③代码里有没有 NativeModules / requireNativeComponent全无
④import 图体检只有 react / react-native + 相对路径 ⇒ 零依赖 ✓
⑤scripts 扫 prepare 家族✅ 没有
npm install "git+https://atomgit.com/oh-react-native/react-native-confirmation-code-field.git#9.0.0-ohos-1.0.0" --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-confirmation-code-field') ],
import CodeFieldTestApp from './CodeFieldTestApp';
AppRegistry.registerComponent('CodeFieldTestApp', () => CodeFieldTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CodeFieldTestApp

不需要 HAR、权限、ohpm、link-harmony,也不需要补装任何依赖。


三、读源码取到的真值

交付包只有 esm/**(tsc 产物),所以以下引用都来自 esm/**。

3.1 导出与形态

// esm/index.js
export { Cursor } from './Cursor';
export { CodeField } from './CodeField';
export { MaskSymbol } from './MaskSymbol';
export { useClearByFocusCell } from './useClearByFocusCell';
export { useBlurOnFulfill } from './useBlurOnFulfill';
export { isLastFilledCell } from './utils';

实测:命名空间正好 6 个 键;CodeField / Cursor / MaskSymbol 都是函数组件(箭头函数 ⇒ 没有 prototype.render),且都设了 displayName:

displayName = CodeField  Cursor  MaskSymbol

3.2 ★★ CodeField 的核心:把"每格该显示什么"算出来

export const CodeField = ({ rootStyle, textInputStyle, value, renderCell,
  cellCount = DEFAULT_CELL_COUNT /* 4 */, RootProps, RootComponent = View,
  InputComponent = TextInput, ref, ...rest }) => {
  'use memo';
  const [isFocused, onFocus, onBlur] = useFocusState(rest.onBlur, rest.onFocus);
  const symbols = getSymbols(value || '', cellCount);
  const cells = symbols.map((symbol, index, symbols) => {
    const isFirstEmptySymbol = symbols.indexOf('') === index;
    return renderCell({ index, symbol, isFocused: isFocused && isFirstEmptySymbol });
  });
  return (
    <RootComponent {...RootProps} style={rootStyle ? [styles.root, rootStyle] : styles.root}>
      {cells}
      <InputComponent caretHidden spellCheck={false} autoCorrect={false} blurOnSubmit={false}
        clearButtonMode="never" autoCapitalize="characters" underlineColorAndroid="transparent"
        maxLength={cellCount} autoComplete={autoComplete} {...rest}
        value={value} onBlur={onBlur} onFocus={onFocus}
        style={textInputStyle ? [styles.textInput, textInputStyle] : styles.textInput} ref={ref}/>
    </RootComponent>
  );
};

两个关键点:

  1. ★ isFocused 只对"第一个空格"为真(symbols.indexOf('') === index)⇒ 实测 value="12"、6 格时,只有 index=2 带 FOCUS ✓
  2. ★★ 隐藏输入框是"透明铺满"的:
textInput: {
  ...StyleSheet.absoluteFill,   // 铺满整个 root
  opacity: 0.015,               // 几乎完全透明(用来接收点击)
  fontSize: 1,                  // 让光标/文字极小
},

⇒ 所以点格子区域就等于点那个输入框 ✓(这也是为什么"点某格清空"那种功能可以工作)
⇒ maxLength = cellCount;autoComplete:

const autoComplete = Platform.select({ android: 'sms-otp', default: 'one-time-code' });

⇒ 鸿蒙(不是 android)取 'one-time-code'(与 iOS 同档)

3.3 ★ 三个配套 hook

// useFocusState:自己维护 isFocused,并把事件透传给你
export const useFocusState = (onBlurFromProps, onFocusFromProps) => {
  const [isFocused, setFocusFlag] = useState(false);
  const onBlur  = useCallback((e) => { setFocusFlag(false); onBlurFromProps?.(e); }, [onBlurFromProps]);
  const onFocus = useCallback((e) => { setFocusFlag(true);  onFocusFromProps?.(e); }, [onFocusFromProps]);
  if (__DEV__) { useDebugValue(isFocused ? 'Focused' : 'Unfocused'); }   // ★ 只在 __DEV__
  return [isFocused, onFocus, onBlur];
};

// useBlurOnFulfill:填满就失焦
export const useBlurOnFulfill = ({ value, cellCount }) => {
  const inputRef = useRef(null);
  useEffect(() => { if (value?.length === cellCount) { inputRef.current?.blur(); } }, [value, cellCount]);
  return inputRef;
};

// useClearByFocusCell:点哪一格,就把值截断到那一格之前
const clearCodeByCoords = useCallback((coords) => {
  const index = findIndex(coords, cellsLayouts.current);      // 用 locationX/locationY 命中格子
  if (index !== -1) {
    const { value, setValue } = paramsRef.current;
    setValue((value || '').slice(0, index));                  // ★ slice(0, index)
  }
}, []);

⇒ useClearByFocusCell 返回 [inputProps, getCellOnLayoutHandler]:inputProps 里的 onPressOut 负责命中判断,getCellOnLayoutHandler(index) 要挂到每个格子的 onLayout 上(用来记录格子坐标)✓

3.4 ★★ 两个可直接断言的纯函数

// esm/utils.js
export const isLastFilledCell = (value, index) => value != null && value.length - 2 === index;
export const getSymbols = (codeValue, codeLength) => {
  const symbols = new Array(codeLength);
  for (let i = 0; i < codeLength; i++) { symbols[i] = codeValue[i] ?? ''; }
  return symbols;
};

实测(不用渲染就能验):

getSymbols("12", 4)      → ["1","2","",""]
getSymbols("", 3)        → ["","",""]
getSymbols("123456", 3)  → ["1","2","3"]      ← 截断到 codeLength
isLastFilledCell("1234", 2) === true          ← 语义是"倒数第二格"
isLastFilledCell("1234", 3) === false
isLastFilledCell("12", 0)   === true          ← 2-2=0
isLastFilledCell(null, 0)   === false

3.5 闪烁与掩码

// Cursor:每 delay(默认 500ms) 在 "|" 与 "" 之间切换
useInterval(toggleVisibility, delay);
return <>{visibleFlag ? cursorSymbol : ''}</>;

// MaskSymbol:500ms 后翻一次面;isLastFilledCell 为真时立刻隐藏
useTimeout(toggleVisibility, delay);
useEffect(() => { if (isLastFilledCell) { setFlag(false); } }, [isLastFilledCell]);
return <>{visibleFlag ? symbol : maskSymbol}</>;

四、验证设计

判据内容
① 纯函数直调getSymbols / isLastFilledCell 的若干输入输出(不依赖渲染)
② 焦点规则renderCell 把它们全部打日志 ⇒ 核对"只有第一个空格带 FOCUS"
③ 结构6 个导出、三个组件是函数组件、displayName
④ 时序(本轮未完成)Cursor 闪烁 / MaskSymbol 掩码 ⇒ 计划用两次 dump 读文本比对

五、实测结果

5.1 断言 29 / 29

组内容结果
A 契约6 个导出、三个组件是函数组件(无 prototype.render)、displayName、isLastFilledCell 6 例、getSymbols 3 例22/22
B 渲染与几何全部实例渲染未抛错、标题不与状态栏重合、renderCell 调用次数与 FOCUS 记录4/4
C 边界与交互renderCell 日志已产生、两个受控值、判据与边界记录3/3
合计29/29

关键观测值:

命名空间导出 = CodeField, Cursor, MaskSymbol, isLastFilledCell, useBlurOnFulfill, useClearByFocusCell
displayName = CodeField  Cursor  MaskSymbol
getSymbols("12", 4) = ["1","2","",""]     getSymbols("", 3) = ["","",""]     getSymbols("123456", 3) = ["1","2","3"]
★ F1 renderCell 调用次数(cellCount=6) = 18        (3 次渲染 × 6 格)
★ F1 被标记 focused 的格子 = F1 idx=2 sym="" FOCUS
各实例容器尺寸(vp) = {"f1":"416x64"}
UI 安全区:TITLE measureInWindow y=420 > 117 ✓

5.2 ★★ 焦点规则(核心证据)

<CodeField cellCount={6} value="12" /> 时 renderCell 的调用日志:

CELL F1 idx=0 sym="1" -        CELL F1 idx=1 sym="2" -        CELL F1 idx=2 sym="" FOCUS
CELL F1 idx=3 sym="" -         CELL F1 idx=4 sym="" -         CELL F1 idx=5 sym="" -

⇒ symbol 的切分符合 getSymbols(['1','2','','','',''])✓
⇒ FOCUS 只出现在 index=2(第一个空格) ✓✓ 源码里 isFocused && isFirstEmptySymbol 的语义被完整证实

在这里插入图片描述

5.3 其它

  • F1 的 root 尺寸 = 416×64 vp(格子 44vp + 上下 padding 10vp)⇒ 一排格子铺满可用宽度 ✓
  • F1 的 renderCell 共被调 18 次 = 3 次渲染 × 6 格 ✓(说明它是"每格每次渲染都回调")

在这里插入图片描述

在这里插入图片描述


六、★ 我自己踩的两个 React 坑(本篇最值得记的部分)

6.1 坑一:把事件对象读进 setState 的 updater ⇒ 整页红屏

我最初这样测量容器尺寸:

onLayout={e => setGeoms(p => ({...p, f1: `${Math.round(e.nativeEvent.layout.width)}x…`}))}
//                             ^^^^^^^^^^^^^^^ 在 updater 内部读事件对象

结果:页面启动后整屏被 LogBox 红屏盖住,报

Render Error
Cannot read property 'layout' of null
Component Stack: <CodeFieldTestApp /> → <View_withRef /> → <View_withRef />
Call Stack: anonymous → basicStateReducer → updateReducerImpl → updateReducer → useState

排查过程(值得记):

  1. 先看到"页面标题 dump 不到、日志一条没有";
  2. 用 devecocli ui layout 打印整棵 dump ⇒ 最外层是 Dialog,里面有 logbox_message_title_text "Render Error" 和 logbox_message_contents_text "Cannot read property 'layout' of null",以及 Component Stack / Call Stack;
  3. 调用栈里的 basicStateReducer 直接指出"错误发生在 useState 的 updater 里" ⇒ 定位到我自己那行。

为什么错:setGeoms(p => …) 的 updater 函数不是在调用时执行,而是在下一次渲染的 reducer 阶段执行 ⇒ 那时再去读 e.nativeEvent,事件对象已经不保证可用(这里是 null)。

修法:先把值取出来,updater 里只用普通数值:

onLayout={e => {
  const w = e?.nativeEvent?.layout?.width;
  const h = e?.nativeEvent?.layout?.height;
  const txt = `${Math.round(w ?? 0)}x${Math.round(h ?? 0)}`;
  setGeoms(p => ({...p, f1: txt}));     // ✅ updater 里只用 txt
}}

6.2 坑二:在 renderCell 里 setState ⇒ 渲染期更新别的组件

我为了把每格日志显示到页面上,在 renderCell 回调里调了 setCellLog(...)。LogBox 立刻报:

Cannot update a component (`CodeFieldTestApp`) while rendering a different component

为什么错:renderCell 是 CodeField 在渲染期间调用的(symbols.map(...) 就在 render 里)⇒ 此时调用 setState 属于 “渲染期更新另一个组件” ✗ React 明确禁止。

修法:renderCell 里只写 ref + console.log,不碰 state;等用户点按钮时再从 ref 同步到 state 用于展示:

const logCell = (msg: string) => {
  logRef.current = [...logRef.current, msg].slice(-40);   // ✅ 只写 ref
  console.log(`[codefield-test] CELL ${msg}`);            // ✅ 只打日志
};

教训:“回调"不等于"可以在里面随便改状态” —— 要分清这个回调是事件回调(点击、滚动,可以 setState)还是渲染期回调(renderCell / renderItem / renderText 这类由父组件在 render 里调用的,不能 setState)。


七、已知限制与本次验证边界

7.1 用这个库要知道的

事项说明
格子由你自己渲染组件只给 renderCell({index, symbol, isFocused}),没有内置外观
isFocused 的含义只对"第一个空格"为真(不是"当前光标格")
隐藏输入框铺满 rootabsoluteFill + opacity 0.015 ⇒ 点任意格子区域都会命中它
autoComplete 在非 android 平台是 'one-time-code'鸿蒙取这个分支
isLastFilledCell(value, index) 的语义value.length - 2 === index ⇒ “当前值的倒数第二格”,容易误读
useClearByFocusCell 是"截断"点第 N 格会把值截成前 N 位(slice(0, index)),不是"清空该格"
useBlurOnFulfill 按长度判断value.length === cellCount 才失焦
useFocusState 的 useDebugValue 只在 __DEV__生产环境不执行

7.2 本次验证的边界

  1. Cursor 的闪烁与 MaskSymbol 的掩码时序【未完成验证】 —— 原计划"两次 dump 读文本比对",两次文本提取都写错了(一次区间取到 0、一次匹配式不严谨),没有拿到有效结果 ⇒ 只做了源码级记录,没有实测;
  2. useClearByFocusCell 的命中逻辑未做交互实测(需精确点到某格并观察值被截断;本轮只验证了受控值存在与 hook 可用);
  3. useBlurOnFulfill 的失焦未实测(需真的输入到 cellCount 位);
  4. 焦点交互复查未得到确定结论:我试过"点击 F1 区域后再看 FOCUS 日志",清日志后未再出现 FOCUS(点击前后语义不明确)⇒ 未把"点击使其聚焦"作为结论;
  5. autoComplete='one-time-code' 只是源码级结论(未在设备上验证实际自动填充行为);
  6. RootComponent / InputComponent 自定义容器未验证;
  7. useClearByFocusCell 的 web 变体(useClearByFocusCell.web.js)与 CodeField.styles.web.js 未验证;
  8. 只在一台模拟器上验证(Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。

未改动库代码。


八、常见问题

Q1:怎么用?

import {CodeField, Cursor} from 'react-native-confirmation-code-field';

const [value, setValue] = useState('');
<CodeField
  value={value}
  onChangeText={setValue}
  cellCount={6}
  keyboardType="number-pad"
  textContentType="oneTimeCode"
  renderCell={({index, symbol, isFocused}) => (
    <View key={index} style={[styles.cell, isFocused && styles.focusCell]}>
      <Text>{symbol || (isFocused ? <Cursor /> : null)}</Text>
    </View>
  )}
/>

Q2:为什么"只有第一个空格"会显示光标?

因为组件给 renderCell 的 isFocused 是 isFocused && isFirstEmptySymbol(symbols.indexOf('') === index)—— 它的语义是"当前应该输入的位置",不是"光标所在格"。实测 value="12"、6 格时只有 index=2 为真。

Q3:怎么让填满后自动收起键盘?

const ref = useBlurOnFulfill({value, cellCount: 6});
<CodeField ref={ref} value={value} cellCount={6} … />

Q4:想"点第 3 格就从第 3 格开始重输"怎么做?

const [props, getCellOnLayoutHandler] = useClearByFocusCell({value, setValue});
<CodeField {...props} value={value} onChangeText={setValue} renderCell={({index, symbol, isFocused}) => (
  <View key={index} onLayout={getCellOnLayoutHandler(index)} …>{symbol}</View>
)} />

⇒ 点第 N 格会把值截断成前 N 位(不是清空该格)。

Q5:isLastFilledCell 怎么用?

<MaskSymbol isLastFilledCell={isLastFilledCell(value, index)} maskSymbol="•">{symbol}</MaskSymbol>

⚠️ 它的判断是 value.length - 2 === index ⇒ “倒数第二格”(想表达"最后一个已填格"要留意这个差 2)。

Q6:为什么我这排格子点哪儿都能聚焦?

因为库在格子下面铺了一个 absoluteFill + opacity: 0.015 的 TextInput ⇒ 整块区域都是它的点击范围。

Q7:我想换掉外层的 View 或输入框?

用 RootComponent / InputComponent 两个 prop 替换(默认分别是 View 与 TextInput)。

Q8:这个库需要鸿蒙侧做什么特殊处理吗?

不需要。 纯 JS、零依赖、无原生模块、不需要 HAR / 权限 / ohpm。而且它的 peerDependencies 明确写了 react >= 19,属于面向新 React 的库。


小结

react-native-confirmation-code-field 是纯 JS 的验证码输入组件(零依赖、peerDependencies 明确要求 react >= 19)。设备侧 29 / 29 断言通过。

最值得记下来的是这几条:

  1. ★★ "渲染期回调"里不能 setState。 我在 renderCell(由 CodeField 在 render 里调用)中调了 setCellLog,LogBox 直接报 Cannot update a component while rendering a different component。⇒ renderCell / renderItem / renderText 这类回调只应写 ref 或打日志,要改状态得等事件回调。

  2. ★★ 不要把事件对象读进 setState 的 updater 里。 我写成 setGeoms(p => …e.nativeEvent.layout…) ⇒ updater 在下一次渲染的 reducer 阶段才执行,届时事件对象已不可用(null)⇒ 整页红屏(Cannot read property 'layout' of null)。
    ⇒ 修法:先把值取出来,updater 里只用普通数值。

  3. ★ 红屏也能用 dump 读出来。 这次页面标题 dump 不到、日志一条没有,靠 devecocli ui layout 打印整棵树才发现最外层是 Dialog、里面有 logbox_message_title_text "Render Error" 与 Component Stack / Call Stack,从 basicStateReducer 一眼定位到"错在 useState 的 updater 里"。⇒ 界面被弹窗盖住时,先去 dump 里找弹窗的文本,而不是盲猜。

  4. ★ 能直接调的纯函数优先直接调。 getSymbols / isLastFilledCell 不需要渲染就能验:getSymbols('12',4) → ['1','2','','']、isLastFilledCell('1234',2) → true(语义是倒数第二格)。⇒ 这类断言最快、最稳、零环境依赖。

  5. ★ 借助 renderCell 这类"每格都回调"的钩子做页内观测。 我把每格的 (index, symbol, isFocused) 全打进日志,于是**"焦点只落在第一个空格"这条规则被一次性证实**(实测只有 idx=2 带 FOCUS)。

  6. 没做完的要如实写。 Cursor 闪烁 / MaskSymbol 掩码的时序判据本轮两次尝试都写错了,最终没有拿到实测结果 ⇒ 文章里明确标注"未完成验证",只留源码级记录。


本篇用到的库

项内容
三方库react-native-confirmation-code-field(上游 9.0.0 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-confirmation-code-field
适配 TAG9.0.0-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS,零依赖)
库类型纯 JS 组件库:验证码 / PIN 输入(CodeField + Cursor + MaskSymbol + 3 个 hook/工具)
运行时依赖✅ 零
peerDependenciesreact >= 19.0.0、react-native >= 0.78.0
prepare / postinstall✅ 都没有
许可证MIT
上游仓库https://github.com/retyui/react-native-confirmation-code-field(★1.2k)
宿主工程RNOH084Demo(测试页 rnAppKey = CodeFieldTestApp)
# 从适配仓库安装(零依赖,无需补装)
npm install "git+https://atomgit.com/oh-react-native/react-native-confirmation-code-field.git#9.0.0-ohos-1.0.0" --ignore-scripts

# 本地目录接入
npm install ../react-native-confirmation-code-field --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-confirmation-code-field') ],
import {CodeField, Cursor, MaskSymbol, useBlurOnFulfill, useClearByFocusCell, isLastFilledCell}
  from 'react-native-confirmation-code-field';

const CELL_COUNT = 6;
const [value, setValue] = useState('');

// ① 基本用法(格子外观完全由你决定)
<CodeField
  value={value}
  onChangeText={setValue}
  cellCount={CELL_COUNT}
  keyboardType="number-pad"
  renderCell={({index, symbol, isFocused}) => (
    <View key={index} style={[styles.cell, isFocused && styles.focusCell]}>
      <Text style={styles.txt}>{symbol || (isFocused ? <Cursor /> : null)}</Text>
    </View>
  )}
/>

// ② 填满自动失焦
const ref = useBlurOnFulfill({value, cellCount: CELL_COUNT});
<CodeField ref={ref} … />

// ③ 点某格从该格重输(会把值截断到该格之前)
const [cellProps, getCellOnLayoutHandler] = useClearByFocusCell({value, setValue});
<CodeField {...cellProps} value={value} onChangeText={setValue}
  renderCell={({index, symbol, isFocused}) => (
    <View key={index} onLayout={getCellOnLayoutHandler(index)} …>{symbol}</View>
  )} />

// ④ 掩码(注意 isLastFilledCell 的语义是"倒数第二格")
<MaskSymbol isLastFilledCell={isLastFilledCell(value, index)} maskSymbol="•">{symbol}</MaskSymbol>

// ⚠️ 在 renderCell 里【不要】setState:它是渲染期回调
// ⚠️ 不要把事件对象读进 setState 的 updater 里
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CodeFieldTestApp

判据的复现方式

# ① 纯函数(最快、零环境依赖):
#    getSymbols('12', 4) → ['1','2','',''];isLastFilledCell('1234', 2) → true
# ② 焦点规则:在 renderCell 里 console.log(index, symbol, isFocused)
#    然后 grep 日志:cellCount=6、value='12' ⇒ 只应有 idx=2 带 FOCUS
hdc shell "hilog -x | grep codefield-test" | Select-String 'CELL F1.*FOCUS'

# ③ 页面被弹窗盖住时:先 dump 整棵树找弹窗文本(LogBox 会给出 Render Error 与调用栈)
devecocli ui layout
#    ⇒ 找 logbox_message_contents_text / logbox_stack_frame_text

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.js / npmv24.14.0 / 11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 26(26.0.0.32)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3
本机状态栏高度117px(页面留白取 48vp)
宿主依赖✅ 无需补装(库零依赖)
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 首轮 4 分 00 秒(红屏);修 bug 后 4 分 13 秒、4 分 06 秒 ⇒ 最终 29/29
验证规模设备侧 29 / 29 断言;纯函数直调 9 例(getSymbols 3 + isLastFilledCell 6);焦点规则 1 组(6 格逐格日志,仅 idx=2 带 FOCUS);结构 6 项;UI 安全区 1 项;两个自定义 bug 的定位与修复

相关社区与项目地址

项目地址
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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐