news 2026/9/9 1:42:50

OpenHarmony上React Native数值输入Hook:useNumber设计与踩坑实录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenHarmony上React Native数值输入Hook:useNumber设计与踩坑实录

在 OpenHarmony 上跑 React Native,最磨人的往往不是业务逻辑,而是那些看起来不起眼的输入控件。特别是数字输入——商品数量、价格、分数、年龄、经纬度,哪个页面少了数值输入都难受。我在把一套 RN 页面迁到 OpenHarmony 真机时,发现原生 TextInput 的键盘类型、最大长度限制在部分版本上并不像安卓/iOS 那样稳定,这才下了决心把数值校验收进一个自定义 Hook 里,于是有了 useNumber。

这篇就聊聊 useNumber 的设计过程、在 OpenHarmony 上的踩坑经验,以及一套可以直接抄下来用的代码。它不是一个“读一下思路”的抽象概念,而是连边界条件都处理好的可落地实现。适合正在做 OpenHarmony + RN 跨端开发、或者想在 RN 项目里统一数值输入逻辑的开发者参考。不管你是刚在模拟器上把空白工程跑起来,还是已经折腾了几个月的真机适配,这篇文章都会对你有帮助。

1. 先想清楚再动手:为什么数值校验不能只靠原生控件

1.1 原生控件限制与 RN 桥接的差异

做 RN 开发的人经常默认一件事:<TextInput keyboardType="numeric" />就能拿到数字。在安卓和 iOS 上,这套逻辑大体上没问题,但在 OpenHarmony 的适配层上,事情就没那么简单了。OpenHarmony 的 RN 运行时(社区通常叫 RNOH,React Native OpenHarmony)把 JS 侧的 TextInput 映射到 ArkUI 的 TextInput 组件上,键盘类型、maxLength、软键盘的确认按钮类型,这些属性需要底层桥接层逐一转换。

我实测下来,部分 OpenHarmony 版本上,keyboardType="numeric"弹出的仍然可能是全键盘,maxLength在某些输入法组合下也可能出现“中文输入法候选词越过长度限制”的怪现象。这时候如果业务侧没有兜底,用户就能把字母、特殊符号、甚至整段话输进去。等到提交时后端返回 400,你再去排查接口参数,浪费的时间远比封装一个 Hook 要多。

所以,与其依赖“键盘类型 + maxLength”这种组合拳,不如在逻辑层加一道数值闸门。我们要保证:不管键盘弹出来是什么样,用户输进去的内容必须经过清洗,不符合数值规则的一律拦掉。

1.2 自定义 Hook 的边界划分

在动手写代码前,我明确了一条原则:Hook 只负责“数值输入的状态管理与约束”,不负责 UI 渲染。这样设计的好处是,你可以在任何 TextInput 上用它,也可以封装成带前缀后缀的货币输入框、步进器、滑杆数值显示,UI 怎么变,逻辑不用动。

具体边界如下:

  • 输入清洗:去掉非数字字符、处理多个小数点、处理负号位置。
  • 范围钳制:超过 min/max 时自动拉回边界值,但要注意“输入过程中”和“失焦后”两个阶段的差异化处理。
  • 格式规范:整数模式不允许小数点,小数模式限制精度位数。
  • 状态输出:给 TextInput 提供valueonChangeText,再暴露isValidnumberValue等派生状态。

不做什么也重要:不做“千分位格式化”,因为输入过程中格式化会带来光标跳动问题,体验很难做好;不做 Toast 弹窗和错误展示,因为不同业务对错误状态的呈现方式不一样,工具层不应该替业务做决定。

1.3 设计目标与方案选型

确定边界之后,我梳理出了 useNumber 的核心设计目标:

第一,收敛性。所有数值输入规则在一个地方维护,不管页面里有多少个数字输入框,行为保持一致。第二,可控性。使用者能配置 min、max、integer、precision、允许负数等选项,而不是打开源码改逻辑。第三,稳定性。在 OpenHarmony 的 TextInput 事件异常时,能通过清洗函数把脏数据挡在状态之外。

方案选型上,我决定用 useState 管理字符串,而不是直接用 number。原因是:用户输入“12.” 或者 “-” 的时候,如果状态是 number,这些中间态根本无法表达。字符串状态才符合真实输入过程。

2. 能把 Demo 跑起来:设备树、模拟器与白屏排查

2.1 设备树到底怎么选:rk3568 的“选择困难症”

很多人在 OpenHarmony 环境上栽的第一个跟头,不是代码,而是板子跑不起来。尤其是 rk3568 这类芯片,网上能搜到的开发板型号五花八门,源码里的设备树文件也一堆,什么 evb1、evb2、evb3,还有不同厂家定制的板型。选错了,轻则 WiFi/蓝牙不工作,重则内核跑飞、串口无输出。

我这里的排查顺序供你参考:

  1. 先用串口或 adb 进入系统,执行cat /proc/device-tree/model,看内核实际加载的板子型号,有的板子会直接输出类似“Rockchip RK3568 EVB2 DDR4 V1.0 Board”这样的字符串。
  2. 如果这一步没有输出,看源码里device/boardvendor目录下是否有对应厂家的工程,重点看.dts文件的model字段。
  3. 实在分不清,就用 SDK 默认的 rk3568 配置先跑起来,因为默认配置通常对应官方 EVB 板,外设驱动覆盖范围最全。之后再按板子的实际屏幕、触摸 IC、WiFi 模组调整。

选设备树这件事,要在开始做 RN 联调前就确认好,否则你辛辛苦苦把应用打进去了,屏幕不亮或者触摸不动,根本没法定位是应用问题还是底层问题。

2.2 x86 模拟器与真机的差异

有段时间我图方便,想直接在 x86 版 OpenHarmony 模拟器上跑 RN 工程。想法很美好,但现实是 RN 的某些原生模块依赖 CPU 架构的 so 库,Release 包在 x86 上加载时,经常报“library not found”或者干脆静默失败。我的建议是:模拟器用来验证 JS 层业务逻辑可以,但涉及原生桥接、相机、定位、键盘弹出等能力,最终一定以 ARM 真机为准。

如果非要在 x86 模拟器上调试,记得检查构建产物里是否包含x86_64的二进制,以及 metro 服务是否能在模拟器网络环境下被访问到。别问我为什么强调这一点,问就是调了半天发现模拟器访问不了宿主机 8081 端口,页面一直白屏。

2.3 React Native 启动白屏排查思路

“react native 启动白屏”是我搜烂了的关键词。在 OpenHarmony 上出现白屏,原因通常集中在这几个地方:

  • Bundle 没加载到:Dev 模式检查 metro 是否启动,以及模拟器/真机和电脑是否在同一网络;Release 模式检查index.harmony.bundle是否已正确打包进 hap 资源目录。
  • 原生模块注册失败:RNOH 的包列表里没有注册自定义模块,或者模块初始化抛异常,页面渲染到一半被中断。
  • SDK/RN 版本不匹配:OpenHarmony SDK 的 API Level 和 React Native 适配版本有对应关系,跨大版本组合容易白屏。

排查时直接用 hilog 过滤关键词:hilog | grep RN,能看到加载链路走到哪一步。白屏问题最怕瞎猜,日志能帮你把问题范围缩小到“JS 层”还是“原生层”。这套排查思路,在我做 useNumber 联调时也反复用到。

3. useNumber 实现细节:从基础版到完整版的核心代码

3.1 基础版:min/max 范围钳制

先给一个最简可用的版本。它做的事情很纯粹:清洗输入字符串,用 parseFloat 转成数字,超出 min/max 时把展示值改成边界值。

import { useCallback, useState } from 'react'; interface UseNumberResult { value: string; onChangeText: (text: string) => void; onBlur: () => void; isValid: boolean; } export function useNumber( initialValue: number | string, min?: number, max?: number, ): UseNumberResult { const [value, setValue] = useState<string>(String(initialValue ?? '')); // 清洗:只保留数字、小数点和负号 const clean = useCallback((text: string): string => { if (text === '') return ''; let result = text.replace(/[^0-9.-]/g, ''); // 负号只允许出现在第一位 const minusIndex = result.indexOf('-'); if (minusIndex > 0) { result = result.slice(0, minusIndex) + result.slice(minusIndex + 1); } if (minusIndex === 0 && result.indexOf('-', 1) !== -1) { result = '-' + result.slice(1).replace(/-/g, ''); } // 多个小数点时只保留第一个 const firstDot = result.indexOf('.'); if (firstDot !== -1) { result = result.slice(0, firstDot + 1) + result.slice(firstDot + 1).replace(/\./g, ''); } return result; }, []); const clamp = useCallback( (n: number): number => { let result = n; if (min !== undefined && result < min) result = min; if (max !== undefined && result > max) result = max; return result; }, [min, max], ); const handleChangeText = useCallback( (rawText: string) => { const cleaned = clean(rawText); if (cleaned === '') { setValue(''); return; } const num = parseFloat(cleaned); if (Number.isNaN(num)) return; // 输入阶段:如果值合法就直接更新,超范围则钳制 const clampedNum = clamp(num); if (clampedNum === num) { setValue(cleaned); } else { setValue(String(clampedNum)); } }, [clean, clamp], ); const handleBlur = useCallback(() => { if (value === '') { setValue(min !== undefined ? String(min) : ''); return; } const num = parseFloat(value); if (Number.isNaN(num)) return; setValue(String(clamp(num))); }, [value, min, clamp]); const num = parseFloat(value); const isValid = !Number.isNaN(num) && (min === undefined || num >= min) && (max === undefined || num <= max); return { value, onChangeText: handleChangeText, onBlur: handleBlur, isValid, }; }

这里面有一个很容易被忽略的点:parseFloat('-')返回的是NaN,而用户在输入负数时,第一步就是要单独输入一个负号。如果onChangeText里遇到NaN就直接 return,用户会发现负号根本打不进去。所以我在cleaned === ''时允许直接 setValue 为空字符串,但-会被 parseFloat 解析成 NaN 直接丢弃。

针对这个问题,更稳妥的做法是把-这种“不完整但合法”的中间态放行:

if (cleaned === '-' || cleaned === '.') { setValue(cleaned); return; }

3.2 完整版:支持整数、小数位与负数开关

基础版只解决了范围问题,实际业务里还有一些高频诉求:只能输入整数、保留两位小数、禁止负数。这些规则如果每个页面写一遍,一定有人漏判。完整版把这些规则收敛进 options,用起来就省心很多。

export interface UseNumberOptions { min?: number; max?: number; integer?: boolean; precision?: number; allowNegative?: boolean; defaultValue?: number | string; } export function useNumber(options: UseNumberOptions = {}) { const [value, setValue] = useState<string>( String(options.defaultValue ?? ''), ); const clean = (text: string): string => { if (text === '') return ''; let result = text.replace( options.allowNegative ? /[^0-9.-]/g : /[^0-9.]/g, '', ); // 负数处理 if (options.allowNegative) { const minusCount = result.split('-').length - 1; if (minusCount > 1) { result = '-' + result.replace(/-/g, ''); } else if (result.indexOf('-') > 0) { result = result.replace(/-/g, ''); } } // 整数模式:直接去掉小数点 if (options.integer) { result = result.replace(/\./g, ''); return result; } // 多个小数点的清理 const firstDotIndex = result.indexOf('.'); if (firstDotIndex !== -1) { result = result.slice(0, firstDotIndex + 1) + result.slice(firstDotIndex + 1).replace(/\./g, ''); } // 小数位精度限制 if (options.precision !== undefined && firstDotIndex !== -1) { const parts = result.split('.'); if (parts[1].length > options.precision) { parts[1] = parts[1].slice(0, options.precision); result = parts.join('.'); } } return result; }; const handleChangeText = (rawText: string) => { let cleaned = clean(rawText); if (cleaned === '' || cleaned === '-' || cleaned === '.') { setValue(cleaned); return; } // 负号后面还没数字时直接放行 if (cleaned === '-' && options.allowNegative) { setValue(cleaned); return; } const num = parseFloat(cleaned); if (Number.isNaN(num)) return; // 输入期间的钳制策略:对于负数场景,要防止 min 是 0 时把负数拦掉 if ( options.min !== undefined && options.max !== undefined && options.min >= 0 && parsedNum < options.min ) { // 不立刻钳制,交给 onBlur 处理,避免用户输入 0.05 时首位 0 被误伤 setValue(cleaned); } else { setValue(String(Math.min(options.max ?? num, Math.max(options.min ?? num, num)))); } }; const handleBlur = () => { if (value === '' || value === '-' || value === '.') { setValue(options.defaultValue !== undefined ? String(options.defaultValue) : ''); return; } const num = parseFloat(value); if (Number.isNaN(num)) return; let final = num; if (options.min !== undefined && final < options.min) final = options.min; if (options.max !== undefined && final > options.max) final = options.max; if (options.integer) { final = Math.trunc(final); } if (options.precision !== undefined) { final = parseFloat(final.toFixed(options.precision)); } setValue(String(final)); }; const numberValue = value !== '' && value !== '-' && value !== '.' ? parseFloat(value) : NaN; const isValid = !Number.isNaN(numberValue) && (options.min === undefined || numberValue >= options.min) && (options.max === undefined || numberValue <= options.max); return { value, onChangeText: handleChangeText, onBlur: handleBlur, isValid, numberValue, }; }

写到这里要解释一下为什么handleChangeText里的钳制逻辑写得这么“别扭”。我最初是用最简单的 clamp-on-change,也就是输入“55”时如果 max 是 50,立即变成“50”。但实际联调后发现,这种策略在金额输入场景下非常反人类:用户想输入“5.05”,刚输入完“5.0”,光标还在中间,值就被解析成 5 直接钳到 max 或保留为 5.0,下一步输 5 的时候字符串状态混乱,光标还会自动跳到末尾。

所以完整版里我做了一个关键决策:输入态允许暂时越界,失焦时才强制修正。你可以在页面提交按钮上通过isValid判断当前值是否合法并给出提示,也可以在 onBlur 里自动修正。这样既保证用户体验顺滑,又确保最终提交给接口的一定是合法值。

3.3 用 useCallback 和 useMemo 优化性能

OpenHarmony 上的 RN 应用,性能和主流移动端还是有差距的,特别是在低配 RK3568 设备上,页面渲染一次卡顿感很明显。所以 Hook 内部我建议用useCallback把函数引用稳定下来,避免父组件每次渲染都生成新的函数,导致 TextInput 重新渲染。

这里有一个容易踩的坑:如果clean函数依赖了options对象,而每次 render 时你传入的都是一个字面量{ min: 0, max: 100 },那么useCallback的依赖数组里写[options]就完全没用,因为对象引用每次都在变。

我在实际项目里的解法是:把 options 拆开,在 Hook 入参上直接接收minmaxprecision这些原始值,内部再聚合成配置对象。这样依赖数组可以精确控制:

export function useNumber( options: UseNumberOptions = {}, ) { const { min, max, integer, precision, allowNegative, defaultValue } = options; // 之后的 useCallback 依赖 [min, max, integer, precision, allowNegative, defaultValue] }

另外,numberValueisValid这两个派生值,每次 render 都计算一次成本很低,不需要useMemo。但如果后续你在表单页里用它做联动校验,比如价格下限不能大于上限,可以把计算提取成一个纯函数getNumberState(value, options),方便在多个 Hook 之间共享。

4. 页面落地实战:三个高频场景的联动写法

4.1 商品数量输入:整数 + 最小值兜底

电商购物车、库存盘点、预约人数,这些场景的输入框需求高度一致:只能输正整数,最小是 1,最大是库存数。用 useNumber 封装出来是这样的:

const { value, onChangeText, onBlur, isValid } = useNumber({ min: 1, max: 99, integer: true, defaultValue: 1, }); <TextInput value={value} onChangeText={onChangeText} onBlur={onBlur} keyboardType="number-pad" placeholder="请输入数量" />

注意keyboardType这里我用的是number-pad而不是numeric,从 OpenHarmony 的适配表现来看,number-pad在多数版本上能弹出更干净的数字键盘,减少用户切换到符号键盘输入小数点的概率。不过这个属性在不同版本上表现不一致,所以别把它当成安全边界,真正的安全边界是 useNumber 的 clean 逻辑。

4.2 价格区间输入:带小数点与 max 校验

价格输入要比数量输入更精细,因为用户可能输入“10.5”这种带小数的值,而且价格区间还要联动校验:最低价不能高于最高价。设计时价格框用precision: 2,失焦后自动保留两位小数。

const minPrice = useNumber({ min: 0, max: 9999, precision: 2, defaultValue: '', }); const maxPrice = useNumber({ min: 0, max: 99999, precision: 2, defaultValue: '', }); // 提交时:下限不能大于上限 const isRangeValid = minPrice.isValid && maxPrice.isValid && (maxPrice.numberValue > minPrice.numberValue || Number.isNaN(minPrice.numberValue)); <TextInput value={minPrice.value} onChangeText={minPrice.onChangeText} onBlur={minPrice.onBlur} keyboardType="decimal-pad" placeholder="最低价" /> <TextInput value={maxPrice.value} onChangeText={maxPrice.onChangeText} onBlur={maxPrice.onBlur} keyboardType="decimal-pad" placeholder="最高价" />

这里最容易出问题的是 min 为 0 时的空输入。用户把内容清空后,按我前面说的逻辑,空字符串会被放行,因为用户可能正在重新输入。但如果此时有人在失焦时把 value 设成 0,用户再点进输入框删掉 0 想输新值,就会一直被“钳制”回来,体验非常差。所以我的做法是:defaultValue 为空字符串时,失焦不自动填 0,只有显式传入 defaultValue 才回填。

4.3 步进器联动:按钮加减与输入框共用一套状态

步进器场景下,加减按钮操作的是数字,输入框操作的是字符串,两者如何保持同步?核心思路是用numberValue计算下一步的值,然后反向调用onChangeText把新的数字字符串写回状态。

const quantity = useNumber({ min: 1, max: 99, integer: true, defaultValue: 1, }); const step = 1; const handleIncrease = () => { if (Number.isNaN(quantity.numberValue)) return; const next = Math.min(99, quantity.numberValue + step); quantity.onChangeText(String(next)); }; const handleDecrease = () => { if (Number.isNaN(quantity.numberValue)) return; const next = Math.max(1, quantity.numberValue - step); quantity.onChangeText(String(next)); }; <View style={styles.stepper}> <Pressable onPress={handleDecrease} style={styles.stepBtn}> <Text>-</Text> </Pressable> <TextInput value={quantity.value} onChangeText={quantity.onChangeText} onBlur={quantity.onBlur} style={styles.stepInput} /> <Pressable onPress={handleIncrease} style={styles.stepBtn}> <Text>+</Text> </Pressable> </View>

这套写法的好处是,步进器和手动输入共用同一套清洗、钳制、校验逻辑,不会出现“按钮加出 100、手输只能到 99”的分裂状态。我在实际项目里,还把handleIncreasehandleDecreaseuseCallback包了一层,避免步进器组件频繁重渲染。

5. 真机踩坑实录:问题速查表与独家避坑经验

5.1 常见问题速查表

现象可能原因解决方案
输入字母也能进状态只用了 keyboardType 限制,没做逻辑清洗用 useNumber 的 clean 函数过滤非法字符
达到 max 后无法继续删除每次 onChange 都 clamp,输入态被强制修正改用 clamp-on-blur 策略,输入态允许越界
小数位数不受控只校验是否合法,没有格式化截断配置precision,在 clean 阶段做位数截断
输入-时直接没反应parseFloat('-') 返回 NaN,被 return 拦截-.作为合法中间态放行
中文输入法下长度限制失效原生 maxLength 和组合输入逻辑冲突把 max 判断放在 JS 层,失焦时统一修正
快速连续输入时卡顿onChangeText 链路长,频繁 setState用 useCallback 稳定函数引用,避免无关渲染
真机上 onChangeText 拿到 Object少数 OpenHarmony 版本桥接类型不稳定加一层String(text)强制转换再处理

5.2 独家避坑经验:三个真正值钱的小技巧

第一个技巧是“输入态和失焦态的责任分离”。很多开发者写数值校验,只做一次校验,要么全放在 onChangeText 里,要么全放在提交时。onChangeText 里严格钳制会让输入体验支离破碎,放在提交时又会让用户困惑“为什么刚才还能看到 999,提交就报错”。比较好的做法是:onChangeText 负责清洗、onBlur 负责修正、提交前用 isValid 做最终拦截,三层各管一段,代码不但好写,调试也好定位。

第二个技巧是“不要轻易做千分位格式化”。我知道给金额加逗号很好看,但输入过程中的字符串格式化会带来严重的光标跳动问题。你在 useNumber 的值里插入逗号,TextInput 的 value 和用户光标位置就对不上了。如果业务上必须要展示千分位格式,我建议用一个只读的展示组件,输入框保持原样,失焦后的展示值再做格式化。

第三个技巧是“在 OpenHarmony 上调试时多留日志”。因为 OpenHarmony 的 RN 生态还不够成熟,很多问题的表现非常隐蔽。我在 useNumber 的 clean 函数里临时加过 hilog 输出,观察真机上 TextInput 每一帧输入的原始 text 和清洗后的结果。有一次就发现,某版本在输入“12.” 后,桥接层传给 JS 的值变成了“12”,小数点在原生层就被吞掉了。这种问题不看日志根本定位不了,靠猜可能要折腾几天。

5.3 从 useNumber 到通用表单校验的扩展思路

useNumber 解决的是单一输入框的问题,但表单校验通常意味着多个输入框之间的联动。比如采购单里“订购数量不能大于库存”,或者“折扣价不能高于原价”。这类跨字段校验,我建议不要塞进 useNumber 内部,而是在表单容器这一层做组合判断。

我在项目里常用一个简单的模式:每个 useNumber 实例通过numberValue暴露当前数值,其他字段通过闭包引用它。举个例子,原价输入框的值变化后,折扣价框的 max 跟着变,这时可以给折扣价框加一个 key,让它在原价变化时重新初始化。这个方法看起来很粗暴,但比不停的 unregister/register 字段简单得多,而且不容易漏掉状态同步。

如果字段联动再复杂一些,可以引入 react-hook-form 这类的表单库,配合自定义register走 Controller 模式。不过我个人提醒一句:在 OpenHarmony 的 RN 版本上引入重型表单依赖,要先确认对方是否兼容当前的 RNOH 版本,避免为了省代码反而引入更多版本的兼容性坑。

6. 个人经验:这个 Hook 后续还能怎么扩展

后面如果再往深做,我计划把 useNumber 扩展成 useNumberInput 组件库,内置样式统一的数字输入框、步进器和校验提示。同时考虑把精度控制和范围钳制逻辑提取成纯函数集,做到不依赖 React 也能单测覆盖。毕竟数值校验这块逻辑分支多,边界情况多,单元测试的收益非常直接。

最后再说一个开发心态上的建议:在 OpenHarmony 上做 RN 开发,生态确实比安卓要新不少,遇到问题不要先怀疑自己的代码。多看看原生实现的源码,多打日志复现,多查社区 issue,很多看起来玄学的问题,最后都能定位到某个桥接层的小坑。学会带着“再排查一层”的心态去调试,会比急着找人问更有收获。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 1:42:19

AI Agent的沙箱安全屋:多层隔离设计与TitanIDE实践

当一个天生就具备工具调用能力的 AI Agent&#xff0c;第一次拿到终端权限时&#xff0c;那种感觉就像一个刚从驾校毕业的新手&#xff0c;突然坐进了一辆自动驾驶赛车的驾驶舱。它帮你调接口、写脚本、跑测试、连数据库&#xff0c;效率惊人&#xff0c;但你没看到的是&#x…

作者头像 李华
网站建设 2026/9/9 1:41:38

React Native on OpenHarmony:自定义useList实现列表加载与竞态处理

React Native跑在OpenHarmony上&#xff0c;放在两年前还是不太敢想的事&#xff0c;现在却已经成了很多团队的现实选项。公司现有的RN代码库想快速覆盖鸿蒙生态&#xff0c;又不想再养一支ArkTS原生团队&#xff0c;这类诉求在我接触的项目里越来越常见。列表页又是移动应用里…

作者头像 李华
网站建设 2026/9/9 1:41:35

Auto-Rig Pro 3.40z角色自动绑定插件详解:安装、流程与避坑

简介&#xff1a;面向Blender三维艺术家与动画师的自动绑定工具Auto_Rig_Pro 3.40z资源包&#xff0c;专门解决角色骨骼生成、蒙皮权重分配及IK/FK切换等绑定效率难题。压缩包共9个文件&#xff0c;包含blend工程文件、Python脚本、bmap映射预设及zip扩展模块&#xff0c;bmap预…

作者头像 李华
网站建设 2026/9/9 1:41:21

DeepSeek Harness深度实测:安装、插件与源码解析的大模型工作台全指南

前两天我还在自己博客里写了篇吐槽&#xff0c;说DeepSeek Harness大概率就是官方套壳&#xff0c;把命令行包一层UI就拿出来糊弄人。结果连夜实测了一整个晚上&#xff0c;第二天我就把文章删了&#xff0c;并且回去给梁神道了个歉——这东西的厚度&#xff0c;比我印象里那些…

作者头像 李华
网站建设 2026/9/9 1:39:53

深入理解magnitude:从向量模长到地震震级的核心概念与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 1:39:16

树莓派Pico RP2040 DMA实战:MicroPython下UART收发不卡顿

做嵌入式的小伙伴应该都有过这种经历&#xff1a;跑着一套看似简单的MicroPython脚本&#xff0c;结果串口一打开、数据一多&#xff0c;主循环就开始卡顿。轮询收发会一直占着CPU&#xff0c;中断处理又把正常的执行流打得七零八落。如果你手里正好有一块树莓派Pico&#xff0…

作者头像 李华