
HarmonyKit | 时间戳转换:自动识别秒/毫秒与双向转换器实现
引言:一个让开发者头疼的数字
你拿到一个数字:1735660800。它是秒级时间戳(2024年最后一天的零点)还是毫秒级时间戳(1970年1月21日某个时刻)?如果这是一个秒级时间戳而你的代码按毫秒处理,new Date(1735660800).toISOString() 会给你 1970 年------溢出错误的源头。
这个问题不只是理论上的------它是一个每天都在发生的真实痛点。后端 API 返回的 Unix 时间戳可能是秒也可能是毫秒,完全取决于后端工程师的个人习惯。日志系统可能输出毫秒,数据库可能存储秒。当你作为移动端开发者拿到一个时间戳数字时,你需要手动判断它的单位------或者更糟,错误地假设了它的单位导致显示的日期偏差了几十年。

HarmonyKit 的时间戳转换工具用一行代码解决了这个问题。
项目仓库:https://atomgit.com/VON-/harmony-kit
核心算法:自动识别的 100 亿阈值
TimestampUtils 的核心实现:
typescript
export class TimestampUtils {
static formatDate(timestamp: number, showMillis: boolean = false): string {
let date = new Date(timestamp);
let y = date.getFullYear();
let m = String(date.getMonth() + 1).padStart(2, '0');
let d = String(date.getDate()).padStart(2, '0');
let h = String(date.getHours()).padStart(2, '0');
let min = String(date.getMinutes()).padStart(2, '0');
let s = String(date.getSeconds()).padStart(2, '0');
let ms = String(date.getMilliseconds()).padStart(3, '0');
let result = `${y}-${m}-${d} ${h}:${min}:${s}`;
if (showMillis) {
result += `.${ms}`;
}
return result;
}
static toTimestampSec(dateStr: string): number {
let date = new Date(dateStr);
return Math.floor(date.getTime() / 1000);
}
static toTimestampMs(dateStr: string): number {
let date = new Date(dateStr);
return date.getTime();
}
static nowSec(): number {
return Math.floor(Date.now() / 1000);
}
static nowMs(): number {
return Date.now();
}
}
自动识别逻辑在调用方(TimestampConverter 页面)而不是在工具类中:
typescript
tsToDate() {
let val = Number(this.tsInput.trim());
if (isNaN(val) || this.tsInput.trim() === '') {
this.dateOutput = '请输入有效的时间戳';
return;
}
// auto-detect: if > 10000000000 then it's milliseconds
let ts = val > 10000000000 ? val : val * 1000;
this.dateOutput = TimestampUtils.formatDate(ts, val > 10000000000);
}
typescript
let ts = val > 10000000000 ? val : val * 1000;
这行是自动识别的核心。阈值是 10,000,000,000(100 亿)。逻辑非常直接:
- 如果数字 > 100 亿,假设它是毫秒级时间戳(当前时间的毫秒值约 1,700,000,000,000,远超 100 亿),直接使用
- 如果数字 <= 100 亿,假设它是秒级时间戳(当前时间的秒值约 1,700,000,000,低于 100 亿),乘以 1000 转换为毫秒
这个算法覆盖了 > 99% 的实际场景。为什么不是 100%?因为存在两个边界情况:
- 毫秒级时间戳 < 100 亿:即在 1970 年 1 月 1 日到 1970 年 4 月 26 日之间的时间戳。在实用场景中,几乎没有人会操作这个时间范围的时间戳。
- 秒级时间戳 > 100 亿:即在 2286 年 11 月 20 日之后的未来时间。对于大多数应用来说,这个时间也需要到 200 多年后才成为问题。
100 亿的阈值是经过精心计算的:它在一个"人类不会误入"的安全区域内。毫秒级时间戳在 1970 年后的第 115 天就突破了 100 亿(10000000000 ms = 1970-04-26 17:46:40 UTC),从此再也没有回到过 100 亿以下。秒级时间戳要到 2286 年才会突破 100 亿。所以 1970 年 4 月到 2286 年 11 月这 316 年间,100 亿都可以完美区分秒和毫秒------覆盖了人类有电脑以来的全部时间范围。
为何将自动识别放在页面层而非工具类
注意到 tsToDate() 方法在 TimestampConverter 页面中,而不是在 TimestampUtils 工具类中。这是一个有意的架构选择:
工具类 TimestampUtils 只做"确定的操作":给定一个明确的毫秒时间戳 → 格式化为日期字符串;给定一个日期字符串 → 转换为秒/毫秒时间戳;获取当前时间的秒/毫秒值。工具类不做任何判断或推断------它执行的是"如果输入是 X,输出就是 Y"的确定性转换。
页面层 TimestampConverter 负责"用户意图的解析":用户的输入是一个不确定的数字(可能是秒也可能是毫秒),页面层根据 100 亿阈值推断用户的意图,然后将确定的值传给工具类。页面层处理的是"不确定性的消解"------把模棱两可的用户输入转化为明确的参数。
这种分层让工具类可以在其他上下文中安全复用。如果未来有一个工具需要使用"确定是毫秒的时间戳 → 日期字符串"的转换,它可以直接调用 TimestampUtils.formatDate(ts) 而不需要关心"这个 ts 是秒还是毫秒"------因为调用方已经做了判断。
formatDate 的格式化细节
formatDate 实现了 yyyy-MM-dd HH:mm:ss 格式的日期时间字符串,可选附加毫秒部分:
typescript
let y = date.getFullYear();
let m = String(date.getMonth() + 1).padStart(2, '0');
let d = String(date.getDate()).padStart(2, '0');
let h = String(date.getHours()).padStart(2, '0');
let min = String(date.getMinutes()).padStart(2, '0');
let s = String(date.getSeconds()).padStart(2, '0');
let ms = String(date.getMilliseconds()).padStart(3, '0');
let result = `${y}-${m}-${d} ${h}:${min}:${s}`;
if (showMillis) {
result += `.${ms}`;
}
return result;
date.getMonth() 返回 0-11(JavaScript 的标准行为),需要 +1 转换为 1-12。.padStart(2, '0') 确保单个数字(如 3 月)显示为 "03" 而非 "3"。这对于格式化的一致性和排序性(字符串排序时 "03" 会正确排在 "12" 之前)都是必要的。
showMillis 参数仅在输入是毫秒级时间戳(val > 10000000000)时生效。它通过 formatDate 的第二个参数控制:
typescript
this.dateOutput = TimestampUtils.formatDate(ts, val > 10000000000);
这个设计理念是:如果用户输入的是毫秒级时间戳,说明他们关心毫秒精度,结果中显示毫秒部分是有意义的信息;如果用户输入的是秒级时间戳,毫秒部分总是 ".000",显示出来只会增加视觉干扰。
双向转换的 UI 设计
时间戳转换工具是 HarmonyKit 中唯一一个包含两个独立功能区块的工具页。上半部分处理"时间戳 → 日期时间",下半部分处理"日期时间 → 时间戳"。两个区块之间用 Divider() 分隔:
typescript
// Section 1: Timestamp -> Date
Text('时间戳 → 日期时间')
.fontSize(15).fontWeight(FontWeight.Medium)
.width('100%').padding({ left: 16, top: 16, bottom: 8 });
TextInput({ text: this.tsInput, placeholder: '输入 Unix 时间戳(秒)' })
// ...
Row() {
Button('秒级时间戳').onClick(() => {
this.tsInput = String(TimestampUtils.nowSec());
});
Button('毫秒级时间戳').onClick(() => {
this.tsInput = String(TimestampUtils.nowMs());
});
Blank();
Button('转换').onClick(() => this.tsToDate());
}
// ...
Divider().margin({ left: 16, right: 16 });
// Section 2: Date -> Timestamp
Text('日期时间 → 时间戳')
.fontSize(15).fontWeight(FontWeight.Medium)
.width('100%').padding({ left: 16, top: 16, bottom: 8 });
TextInput({ text: this.diyDate, placeholder: 'yyyy-MM-dd HH:mm:ss' })
// ...
Row() {
Button('填入当前时间').onClick(() => {
this.diyDate = TimestampUtils.formatDate(Date.now());
});
Blank();
Button('转换').onClick(() => this.dateToTs());
}
两个区块虽然布局相似,但在按钮配置上有所不同:
**上半部分(时间戳 → 日期)**有两个快捷按钮:秒级时间戳和毫秒级时间戳。它们将当前时间(以秒或毫秒格式)填入输入框。这解决了"我不知道现在的时间戳是多少"的问题------一键填入,不需要手动计算或去查。
**下半部分(日期 → 时间戳)**有一个快捷按钮:填入当前时间。它将当前日期时间(格式化的字符串)填入输入框。用户可以用这个作为起始点,手动修改日期或时间,然后转换。
这两个区块的快速按钮都使用了灰色背景、12px 小字体的样式,与主操作按钮(蓝色"转换"按钮)形成视觉层级区分。快速按钮是"辅助操作",转换按钮是"主操作"。
日期输入的格式验证
日期转时间戳功能的验证逻辑:
typescript
dateToTs() {
if (!this.diyDate.trim()) {
this.tsOutput = '请输入日期时间';
return;
}
let sec = TimestampUtils.toTimestampSec(this.diyDate);
let ms = TimestampUtils.toTimestampMs(this.diyDate);
if (isNaN(sec)) {
this.tsOutput = '日期格式无效,请使用 yyyy-MM-dd HH:mm:ss';
return;
}
this.tsOutput = `${sec} (秒)\n${ms} (毫秒)`;
}
验证策略分为两层:
- 输入为空 → 提示"请输入日期时间"
- 输入的日期格式无效(
new Date()无法解析) →getTime()返回NaN→ 提示"日期格式无效"
没有使用正则表达式严格验证格式(如必须满足 \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),而是依赖 new Date() 的解析能力。JS 的 Date 构造函数对各种日期格式的解析相当宽泛,它能处理 "2024-01-01"、"2024/01/01"、"01 Jan 2024" 等多种格式。依赖 new Date() 的宽泛解析意味着 HarmonyKit 对用户的输入格式要求不是"死板"的------大多数合理的日期表示都能被正确解析。
但这也有一个风险:不同平台的 new Date() 解析行为有细微差异。在 HarmonyOS 环境中,new Date() 的行为与 Chromium/V8 保持一致,因为 ArkTS 的运行时基于 V8 的一个定制版本。在 Android 的 WebView 或 iOS 的 JavaScriptCore 中,解析结果可能有细微出入。对于 HarmonyKit 这种纯鸿蒙应用来说,这不是问题------它只会在 HarmonyOS 设备上运行。
为什么不用 DatePicker
一些时间戳转换工具会使用原生的日期选择器(DatePicker)让用户通过滑轮选择年/月/日/时/分/秒。HarmonyKit 选择了 TextInput + 占位符的方案。原因有三:
效率优先:目标用户是开发者------他们有输入格式的认知,不需要滑轮翻页。输入 "2024-06-15 14:30:00" 耗时约 3 秒,而通过 6 个滑轮选择年月日时分秒耗时约 15-20 秒。在开发者工具的世界中,效率是最高优先级。
粘贴友好:开发者经常从日志、数据库、API 响应中复制日期字符串。TextInput 天然支持粘贴,DatePicker 不支持。如果用户已经有一个日期字符串在手,DatePicker 反而需要他们"看着字符串、手动在滑轮上翻到对应值"。
一致性:HarmonyKit 的所有 10 个工具页都使用 TextInput/TextArea 作为输入方式。引入 DatePicker 会打破这种一致性------它是唯一一个不使用键盘输入的控件。对于开发者用户来说,"输入"比"选择"更自然。
aboutToAppear 的巧妙运用
TimestampConverter 使用了 aboutToAppear() 生命周期方法在页面初始化时自动转换当前时间戳:
typescript
@Entry
@Component
struct TimestampConverter {
@State tsInput: string = String(TimestampUtils.nowSec());
@State dateOutput: string = '';
@State diyDate: string = '';
aboutToAppear() {
this.tsToDate();
}
// ...
}
初始化时,tsInput 被设置为当前时间的秒级时间戳。aboutToAppear() 在页面即将显示前执行,调用 tsToDate() 自动将这个时间戳转换为日期时间字符串显示在结果区。
这意味着用户打开时间戳转换工具时,不需要做任何操作就能看到当前时间的转换结果------一个"开箱即用"的体验。aboutToAppear() 在 ArkUI 组件生命周期中的地位类似于 React 的 useEffect(() => {}, [])------在组件首次渲染到 UI 后执行一次,适合做初始数据加载或自动计算。
结果展示的两个时间戳
当用户输入日期时间进行转换时,结果同时显示秒级和毫秒级时间戳:
typescript
this.tsOutput = `${sec} (秒)\n${ms} (毫秒)`;
用 \n 换行分隔两个值,前面的带括号标注 (秒) 和 (毫秒)。这种设计消除了"用户拿到了一个数字但不知道它是秒还是毫秒"的问题------转换结果明确告诉你两种格式的值。
CopyButton 可以复制包含两个时间戳的完整字符串(包括换行符)。如果用户只需要其中一个,他们可以手动选择复制,或者先粘贴到其他地方再删除不需要的部分。这是"提供最大信息量"的设计------给你两个值,你自己决定要哪个。
总结
时间戳转换工具的核心价值在于:自动消除时间戳的单位歧义。100 亿这个简单的阈值覆盖了 99.9% 的实际场景------1980 年到 2200 年之间,100 亿可以完美区分秒级和毫秒级时间戳。
架构层面上,它将"自动识别"(页面层的职责)和"确定性转换"(工具类的职责)分了开来------工具类不做猜测,页面层做用户意图的解析。这种分层让工具类可以被其他组件安全复用。交互层面上,双向转换、快捷填入按钮、aboutToAppear 的初始化、秒/毫秒双输出------这些设计共同构成了一个"开箱即用"的时间戳处理体验。