鸿蒙 PC 开源实战:用 ArkTS 写一个「活动签到查重 + 参与统计」应用(附 10 个编译避坑点)
摘要活动签到数据里最烦人的三件事——同一份文件导了两次、同一个人进出会场签了好几次、姓名或时间字段是脏的。本文从零实现一个面向鸿蒙 PCHarmonyOS NEXT / 2in1的开源 ArkTS 应用把这三类问题一次性处理掉导入 CSV → 自动判重 → 统计出席率 → 一键导出汇总。全程本地处理不联网、不申请任何权限。关键词HarmonyOS、ArkTS、鸿蒙 PC、2in1、CSV 解析、数据去重、DevEco Studio一、为什么要做这个组织一场线下活动签到环节通常由第三方系统扫码、企业微信、问卷导出成 CSV。拿到文件之后组织者真正想知道的只有两件事这份数据干净吗能不能直接拿去算绩效 / 报人数到底来了多少人不是签到次数而是去了重的人头数。手工做这件事的人都会踩到同样的三个坑坑真实表现后果重复导入同一份文件导两次或扫码之后又手工补录一次实到人数虚高同一秒出现多条记录多次签到一天里进出会场多次签退也被算成签到出席率算不准无法区分来了和来了好几趟脏数据姓名缺失、时间写成待定、活动 ID 为空整个统计静默出错还找不到是哪一行的问题市面上的签到系统大多只管收数据不管洗数据。所以我干脆自己写一个导入即出结论并且告诉你每一条可疑记录在哪一行。二、成品长什么样五个页签PC 宽屏与窄屏自适应断点 840vp┌──────────────────────────────────────────────────────────┐ │ 活动签到与参与统计 [加载演示数据] [导入签到结果] │ ├──────────────────────────────────────────────────────────┤ │ 数据源当前 379 条原始记录 · 3 场活动 · 118 位参与者 │ ├──────────────────────────────────────────────────────────┤ │ 总览 │ 签到明细 │ 重复检查 │ 参与情况 │ 导出汇总 │ ├──────────────────────────────────────────────────────────┤ │ ┌─ 指标卡 ─┬─ 指标卡 ─┬─ 指标卡 ─┬─ 指标卡 ─┐ │ │ ├─ 数据质量检查 ────────┬─ 活动出席情况 ────┤ │ │ ├─ 签到时间分布 ─────────────────────────────┤ │ └──────────────────────────────────────────────────────────┘用内置的 379 行演示数据跑一遍结果是这样数据行不含表头: 379 完全重复: 34 多次签到: 16 异常: 9 ACT-2026-001 名册 120 人 / 出席 111 人 / 出席率 92.5% ACT-2026-002 名册 120 人 / 出席 90 人 / 出席率 75.0% ACT-2026-003 名册 120 人 / 出席 119 人 / 出席率 99.2%这份演示数据是我故意投毒生成的——10% 完全重复 6% 多次签到 3 场各 3 条异常用来验证统计口径对不对。三、技术选型项目版本DevEco Studio5.0.0 及以上HarmonyOS SDKAPI 125.0.0目标设备2in1鸿蒙 PC/tablet/phone第三方依赖零module.json5里声明deviceTypes: [2in1, tablet, phone]2in1就是鸿蒙 PC 的设备类型。声明之后在鸿蒙 PC 上安装即为桌面窗口应用不需要任何额外适配代码——但窗口尺寸变化必须自己处理见第六章第 6 条。四、架构把统计逻辑从 UI 里彻底剥离这是整个项目最想强调的一点。很多 demo 会把统计逻辑直接写在页面的build()里导致三个问题无法测试、无法复用、改一处崩一片。我做了一个严格的分层pages/Index只负责布局与事件绑定viewmodel/AppData 状态 AppActions 动作utils/Statistics纯函数统计引擎utils/CsvParser纯函数解析器model/Types纯数据定义核心是一个纯函数analyze(records:CheckinRecord[],rosterMap?:RosterMap):AnalysisResult它不 import 任何ohos.*系统能力不碰 UI只做数据变换。带来一个直接好处同一套算法可以用 JavaScript 再实现一遍在 Node 里跑单元测试。这就是tools/verify.mjs存在的原因——30 条断言的算法自检不需要设备、不需要模拟器node tools/verify.mjs一秒出结果。$ node tools/verify.mjs ... 30/30 passed对鸿蒙开发来说这一点价值极高你可以在不打开 DevEco Studio 的情况下验证业务逻辑。五、核心实现5.1 三档判重规则判重不是有重复就删而是要区分性质。我把它分成三档类型判定条件建议处置完全重复Exact同一人 同一活动 同一时间戳直接清理多次签到Repeat同一人 同一活动 不同时间人工确认是否算有效出席数据异常Invalid活动 ID / 参与者 ID 为空或时间无法解析回原表修正关键设计每个「人 活动」组合只保留时间最早的一条作为有效出席后续记录标记但不重复计数。// ---- 第二趟组内按时间排序标记 Exact / Repeat 与 Primary ----personGroups.forEach((bucket:AnalyzedRecord[]){// 时间升序时间相同按原始行号保证结果稳定可复现bucket.sort((a:AnalyzedRecord,b:AnalyzedRecord){if(a.record.timestamp!b.record.timestamp){returna.record.timestamp-b.record.timestamp;}returna.record.lineNo-b.record.lineNo;});constseenTimestamps:SetnumbernewSetnumber();for(leti:number0;ibucket.length;i){constitem:AnalyzedRecordbucket[i];if(i0){item.isPrimarytrue;seenTimestamps.add(item.record.timestamp);continue;}if(seenTimestamps.has(item.record.timestamp)){item.kindDupKind.Exact;// 同人同活动同时间exactDupCount;}else{item.kindDupKind.Repeat;// 同人同活动不同时间repeatCount;seenTimestamps.add(item.record.timestamp);}}});注意seenTimestamps用的是Set而不是和上一条比较时间——因为排序后同一秒可能有多条必须是这个时间戳是否出现过而不是是否等于前一条。5.2 实到人数为什么要Set// 实到人数只数 Primary每人一次这是去重后的真实出席人数constprimarySet:SetstringnewSetstring();for(constitemofbucket){rawCount;if(item.kindDupKind.Exact||item.kindDupKind.Repeat){dupCount;}if(item.isPrimary){primarySet.add(item.record.participantId);}}出席人数 去重后的唯一参与者数而不是签到条数。张三签了 3 次实到只 1。这是整个应用最容易被误解、也最必须写清楚的口径。5.3 数据质量分 0~100组织者最需要的其实是一个能不能直接用的判断/** * 数据质量分 0~100。 * 扣分权重异常记录 -10/条完全重复 -3/条多次签到 -1/条。 * 用「每 N 条记录扣 1 分」的方式换算避免数据量大时分数直接归零。 */functioncalcQualityScore(total:number,invalid:number,exact:number,repeat:number):number{if(total0){return0;}constpenaltyRate:number(invalid*10exact*3repeat*1)/total;constscore:number100-penaltyRate*100;returnMath.max(0,Math.min(100,Math.round(score)));}这里有个细节值得说扣分用占比换算而不是直接减绝对分。否则 10000 行的文件里有 20 条重复就归零了分数完全失去区分度。5.4 CSV 列名别名映射真实世界的签到系统导出的列名五花八门活动ID/活动编号/场次ID参与者ID/工号/学号/手机号……我做了两轮匹配精确优先、包含兜底来解决这个问题functionbuildColumnAliases():Mapstring,string[]{constmap:Mapstring,string[]newMapstring,string[]();map.set(activityId,[活动id,活动编号,activityid,活动,场次id,会议id]);map.set(participantId,[参与者id,工号,学号,用户id,userid,人员编号,memberid,手机号,phone,身份证号]);map.set(participantName,[姓名,参与者姓名,用户姓名,员工姓名,name,username,昵称]);map.set(group,[部门,班级,分组,组织,单位,dept,department,group,team,所属部门]);map.set(checkinTime,[签到时间,打卡时间,时间,checkintime,time,createdat,创建时间,签到日期]);map.set(channel,[签到方式,渠道,来源,方式,channel,source,method,签到渠道]);returnmap;}顺便处理了另外四类文件格式坑分隔符自动嗅探逗号 / 分号 / Tab / 竖线Excel 在部分区域设置下导出的是分号RFC 4180 引号转义表示一个引号字符字段内换行不用split(\n)而是手写状态机逐字符扫描UTF-8 BOM导出的 CSV 统一加 BOMExcel 双击不乱码还有一处时间格式兼容覆盖了实际见过的各种写法2026-10-14 09:30:00 2026/10/14 9:30 2026年10月14日 09:30 10 位 / 13 位时间戳六、10 个 ArkTS 编译避坑点本文最有价值的部分这一章是实际把工程跑起来踩出来的。这些错误网上资料零散且大多不报行号 明确原因很容易卡住半天。1.hvigor-config.json5必须存在且modelVersion要和oh-package.json5一致现象Sync 直接失败提示ModelVersion for hvigor is not specified, different from the value of ModelVersion for ohpm, 5.0.0原因DevEco Studio 新建工程时会自动生成hvigor/hvigor-config.json5但如果你是手工/脚本搭的工程很容易漏掉这个文件。解决补建hvigor/hvigor-config.json5modelVersion与根oh-package.json5保持一致。2. 图标资源的层级应用级 vs 模块级不能混现象Sync 报资源找不到。规则引用方资源放哪AppScope/app.json5里的iconAppScope/resources/base/media/entry/src/main/module.json5里的icon/startWindowIconentry/src/main/resources/base/media/且不能重名如果layered_image.json和layered_image.png同名放在一起会冲突。删掉多余的那个。3.app.json5不允许出现 4 个旧字段现象assembleHap报app.json5:11:32。原因新版 schema 不再允许app.json5里出现minAPIVersion/targetAPIVersion/apiReleaseType/debug。SDK 版本统一由build-profile.json5的compatibleSdkVersion/targetSdkVersion管理。解决app.json5只留bundleName/vendor/versionCode/versionName/icon/label。4. 尾随闭包写法要求组件「有且仅有一个」BuilderParam现象In the trailing lambda case, SectionCard must have one and only one property decorated with BuilderParam原因ArkTS 用SectionCard() { ... }这种尾随闭包语法时编译器只能把闭包绑到哪个BuilderParam上产生歧义所以要求有且仅有一个。解决组件里删掉多余的BuilderParam需要多个插槽时改用显式传参不要用尾随闭包。5.ObjectLink只能绑Observed类实例现象编译通过但运行时崩溃。原因ObjectLink要求数据源是被Observed装饰的类实例。AnalysisResult是普通interface不行。解决拆成基础类型Prop传参或者把数据源改成Observed class。6.State只观察第一层现象数据算完了界面不刷新。原因State store: AppStore只观察store自身的变化store.data.result xxx这种深层赋值不触发刷新。解决直接观察需要变的那一层Statedata:AppDataappStore.data;// 直接观察 AppData 的第一层属性并且AppActions必须持有代理后的同一个实例new AppActions(this.data)否则改的不是被观察的那个对象。7.arkts-no-untyped-obj-literals对象字面量必须有明确类型这是报错最多的一条一次编译就报了 10 处。ArkTS 严格模式不允许无类型对象字面量// ✗ 编译失败exportconstLogger{info(msg:string){...}};constconfig{a:1,b:2};// ✓ 用类承载exportclassLogger{staticinfo(msg:string):void{...}}// ✓ 或者显式标注类型constconfig:MyConfig{a:1,b:2};连带注意interface里的嵌套属性也不能写匿名{...}类型必须用具名类型。8.Recordstring, T字面量裸 key 不合法// ✗ 编译失败Record 字面量的 key 没有引号constCOLUMN_ALIASES:Recordstring,string[]{activityId:[活动id],// activityId 被当成标识符};// ✓ 方案 A加引号constM:Recordstring,string[]{activityId:[活动id]};// ✓ 方案 B本项目采用改用 Map复杂场景更灵活constM:Mapstring,string[]newMapstring,string[]();M.set(activityId,[活动id]);9.bindSheet的title字段有类型歧义// ✗.bindSheet($$this.showSheet,this.MySheet(),{title:{title:导入签到结果}})// ✓.bindSheet($$this.showSheet,this.MySheet(),{title:导入签到结果})10. 用命令行跑 hvigor比看 IDE 报错快得多DevEco Studio 的报错面板经常只显示第一条且信息被截断。直接命令行跑完整构建一次拿到全部错误清单exportDEVECO_SDK_HOMED:\DevEco Studio\sdkD:\DevEco Studio\tools\node\node.exe\D:\DevEco Studio\tools\hvigor\bin\hvigorw.jsassembleHap一个提示如果你在纯命令行环境看到spawn java ENOENT那是签名工具需要 Java 而你的 shell 缺 PATHDevEco Studio 内置的 JBR 不受影响不用管它。七、还有两个工程习惯值得保留7.1 导出不申请任何权限导出 CSV 时很多实现会去申请「文件读写」权限用户一拒绝功能就废了。这个应用不申请任何动态权限优先写入应用沙箱的 Download 目录用户可在「文件管理」中直接找到失败时自动落到沙箱files/exports——路径在界面上可一键复制。结论零权限申请的方案在体验上反而比要权限更好。7.2 图标用脚本生成零图形库依赖nodetools/gen-icons.mjstools/gen-icons.mjs是一个手写 PNG 编码器自己实现 zlib CRC32 PNG chunk不依赖sharp/canvas这类原生模块在纯 Node 环境直接跑。好处换配色只需改脚本参数也不会有clone 下来装不上依赖的问题。八、怎么跑起来# GitHubgitclone https://github.com/Hunshow/harmony-checkin.git# 或国内镜像AtomGit / 开放原子gitclone https://atomgit.com/gcw_husamPlM/harmony-checkin.gitcdharmony-checkin用 DevEco Studio 打开工程根目录含oh-package.json5的那一层等待 Sync 完成连接鸿蒙 PC 或启动模拟器点Run ‘entry’首次运行先点「加载演示数据」立刻能看到完整统计效果想验证算法node tools/verify.mjs30 条断言不需要设备Fork 后建议改包名AppScope/app.json5里的bundleName默认是com.example.activitycheckin改成你自己的域名倒写避免冲突。注意改完要重新签名DevEco 的自动签名证书是按原包名申请的包名变了要重新生成。九、可以怎么改二次开发指引想做什么改哪里接入新签到系统的列名CsvParser.ets的buildColumnAliases()调整判重规则Statistics.ets的analyze()第二趟循环改质量分权重Statistics.ets的calcQualityScore()加新的导出报表Statistics.ets加buildXxxCsvRowsIndex.ets加卡片换主题配色common/Tokens.etsUI 层禁止硬编码色值接真实数据源HTTP / 数据库在AppActions.importFromText前加拉取函数下游逻辑全部复用十、项目信息GitHubhttps://github.com/Hunshow/harmony-checkinAtomGithttps://atomgit.com/gcw_husamPlM/harmony-checkin许可证Apache License 2.0规模49 个受版本控制的文件16 个.ets源文件CIGitHub Actions 自动跑node tools/verify.mjs每次提交验证算法欢迎提 Issue / PR。提交前请确保node tools/verify.mjs全部通过如果改了统计逻辑记得同步更新这个自检文件它是 ArkTS 算法的 JS 同构移植不同步的话验证就失真了。十一、总结这个小项目技术上不复杂但有三个点我觉得值得分享把业务逻辑写成纯函数。这是让鸿蒙应用可测试的关键一步——不依赖设备、不依赖 UI性能极差的环境里也能快速迭代。判重要分类不要一刀切。完全重复和多次签到是两种完全不同的业务事实一个可以自动清理一个必须人工确认。混在一起会让工具失去可信度。ArkTS 的严格模式是好事但坑很密集。第六章那 10 条基本覆盖了从零搭建工程到编译通过的全部障碍希望能帮你省下几个小时。如果这篇文章帮你少踩了一个坑欢迎点赞收藏。有更好的判重口径设计也欢迎在评论区交流。