在这里插入图片描述
在这里插入图片描述

一、业务需求(为什么这么设计)

跨国团队最痛的场景是对时间:产品周会定在"北京时间 15:00",伦敦同事要换算成 08:00,纽约同事要知道那是前一天的 03:00。如果每个会议都靠心算时差,迟早有人迟到或错过。

本应用把会议时间统一用 UTC 存储,展示时按每个参会人的时区 + 语言习惯格式化——“3pm UTC” 这个唯一事实,在北京显示 23:00、在纽约显示 11:00 AM,且日期写法各不相同。

二、总体架构

┌─ 数据层:MEETINGS(会议)+ CITIES(城市/时区表)
├─ 格式化层:fmtInTz / fmtStyle / clockTz(intl.DateTimeFormat 三件套)
├─ 文案层:STRINGS 文案表 + t() 降级
└─ 状态层:@StorageLink 语言 + @State now(秒级时钟)/ convertIdx

核心设计:时间只有一个事实(UTC 毫秒时间戳),展示是各时区的投影。业务逻辑里永远操作 utcTime,界面输出时传入 timeZone

数据模型:时区表的两个关键字段

城市表与会议表是这个应用的数据地基,它们的字段设计直接决定上层代码的复杂度:

interface City {
  key: string;
  name: string;      // 显示名
  tz: string;        // IANA 时区
  flag: string;
}

const CITIES: City[] = [
  { key: 'utc', name: 'UTC', tz: 'UTC', flag: '🌐' },
  { key: 'bj', name: '北京', tz: 'Asia/Shanghai', flag: '🇨🇳' },
  { key: 'tokyo', name: '东京', tz: 'Asia/Tokyo', flag: '🇯🇵' },
  { key: 'london', name: '伦敦', tz: 'Europe/London', flag: '🇬🇧' },
  { key: 'ny', name: '纽约', tz: 'America/New_York', flag: '🇺🇸' },
  { key: 'dubai', name: '迪拜', tz: 'Asia/Dubai', flag: '🇦🇪' }
];

设计要点:

  1. tz 存 IANA 名,不存偏移:伦敦的夏令时偏移是 UTC+1、冬令时是 UTC+0——如果数据表里写死 +1,半年后就会差一小时。IANA 名 Europe/Londonintl 自动处理切换;
  2. key 是稳定标识ForEach 的 key 生成器用 c.key(如 'utc''bj'),语言切换时城市名变了但 key 不变,列表不会重建;
  3. flag 是装饰字段:城市名会随语言变化(北京 → Beijing),国旗不翻译,用来锚定视觉。

会议表则把"时区"建模到发起人身上——这是会议产品的关键约定:

interface Meeting {
  id: string;
  topic: string;
  host: string;
  hostTz: string;     // 发起人时区
  utcTime: number;    // 会议时刻(毫秒时间戳)
  durationMin: number;
}

const MEETINGS: Meeting[] = [
  {
    id: 'm1', topic: '产品周会 · Sprint 规划', host: '北京 · 王芳', hostTz: 'Asia/Shanghai',
    utcTime: Date.UTC(2026, 7, 19, 7, 0), durationMin: 60
  },
  {
    id: 'm2', topic: '海外市场同步 · 欧洲 Q3 复盘', host: '伦敦 · Sarah', hostTz: 'Europe/London',
    utcTime: Date.UTC(2026, 7, 19, 14, 30), durationMin: 45
  },
  {
    id: 'm3', topic: '技术评审 · 分布式存储方案', host: '纽约 · David', hostTz: 'America/New_York',
    utcTime: Date.UTC(2026, 7, 20, 2, 0), durationMin: 90
  },
  {
    id: 'm4', topic: '设计走查 · 新首页改版', host: '东京 · 佐藤', hostTz: 'Asia/Tokyo',
    utcTime: Date.UTC(2026, 7, 20, 9, 0), durationMin: 30
  }
];

要点:

  • utcTimeDate.UTC() 构造:不经过本地时区解析,直接指定 UTC 时刻,避免 new Date('2026-08-19 07:00') 这类字符串解析在不同设备上的歧义(有的设备按本地时区解析、有的按 UTC);
  • 不存"发起人本地时间":如果存了 '北京时间 15:00' 这样的字符串,伦敦同事看到的就是一串汉字而不是他自己的时刻——存 UTC、展示时投影,是唯一正确的做法;
  • durationMin 只做展示(“90m” 徽标),不参与时区运算——时长是纯量,与时刻无关。

三、核心实现

3.1 时区格式化(本应用核心)

function fmtInTz(code: string, tz: string, ms: number): string {
  try {
    const fmt = new intl.DateTimeFormat(code, {
      timeZone: tz,        // IANA 时区,如 Asia/Shanghai
      dateStyle: 'full',   // 2026年8月19日星期三
      timeStyle: 'short'   // 14:30 / 2:30 PM
    });
    return fmt.format(new Date(ms));
  } catch (err) {
    return new Date(ms).toString();
  }
}

关键点:

  • timeZoneIANA 时区名Asia/Shanghai),不是 UTC+8 偏移——IANA 名自动处理夏令时(伦敦夏季是 BST,冬季是 GMT,用 Europe/London 一处搞定);
  • dateStyle: 'full' 输出本地化星期与完整日期;
  • 同一 ms 传入不同 timeZone,得到的是同一时刻在各时区的本地表达——这正是"会议排期"的本质。

3.2 会议列表:UTC 存储 + 本地展示

会议数据存 UTC 时间戳(Date.UTC(2026, 7, 19, 7, 0) 表示 UTC 07:00),列表里同时显示"UTC 时刻"(便于对照)和"发起人本地时刻"(用户真正关心的):

private meetingLocal(m: Meeting): string {
  return fmtInTz(this.currentLocale, m.hostTz, m.utcTime);
}

private meetingUtc(m: Meeting): string {
  return fmtInTz(this.currentLocale, 'UTC', m.utcTime);
}

用户切换语言时,currentLocale 变化触发重渲染,所有 fmtInTz 重新执行——同一时刻的显示文字随语言变化。

3.3 世界时钟:秒级刷新

private timer = setInterval(() => { this.now = Date.now(); }, 1000);

function clockTz(code: string, tz: string, now: number): string {
  try {
    const fmt = new intl.DateTimeFormat(code, {
      timeZone: tz,
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false
    });
    return fmt.format(new Date(now));
  } catch (err) {
    return '--:--:--';
  }
}
  • @State now 每秒更新 → 6 个城市的时间行全部重渲染;
  • aboutToDisappear 必须 clearInterval,否则页面销毁后定时器空转(内存泄漏 + 无意义刷新);
  • hour12: false 强制 24 小时制,避免不同 locale 默认 12/24 小时制不一致造成对比困难。

3.4 日期风格对比

同一日期(Date.UTC(2026, 7, 19))在同一 locale 下用四种 dateStyle 展示:

function fmtStyle(code: string, style: string): string {
  try {
    const fmt = new intl.DateTimeFormat(code, {
      dateStyle: style as 'full'
    });
    return fmt.format(new Date(Date.UTC(2026, 7, 19)));
  } catch (err) {
    return style;
  }
}
dateStyle 中文 英文
'full' 2026年8月19日星期三 Wednesday, August 19, 2026
'long' 2026年8月19日 August 19, 2026
'medium' 2026-8-19 Aug 19, 2026
'short' 26-8-19 8/19/26

注意 'short' 的差异最大:中文 26-8-19,英文 8/19/26——顺序完全不同(日月年 vs 月日年)。这就是"格式化交给 intl、UI 不拼日期"的原因。

3.5 时区换算器

固定一个 UTC 时刻(Date.UTC(2026, 7, 19, 7, 0)),选择目标城市后展示该时刻在该城市的本地表达:

Text(`${fmtInTz(this.currentLocale, CITIES[this.convertIdx].tz, Date.UTC(2026, 7, 19, 7, 0))}`)

convertIdx@State,点击城市标签即更新,换算结果实时刷新。

四、文案与降级

与 01 应用相同的 STRINGS + t() 三级降级方案,页面所有字段(会议/时钟/风格/换算)文案随语言切换。语言列表 5 种(简中/繁中/英/日/韩)。

五、ArkTS 兼容要点

  1. timeZone / dateStyle 等选项在 intl.DateTimeFormat 中均有定义,直接传字符串即可(SDK 声明已确认);
  2. setInterval 返回值类型为 number,存私有字段在 aboutToDisappear 清理;
  3. UI 分支内不声明 const,所有派生数据抽成私有方法(meetingLocal() / clockTz());
  4. ForEach key 生成器用稳定值:会议 m.id,城市 c.key
  5. intl 调用全部 try/catch,异常时降级 toString()

六、性能与扩展

  • 秒级刷新只影响 6 行时间文本,ArkUI 增量渲染开销可忽略;若城市上百,应只刷新变化的文本节点;
  • 夏令时陷阱:直接存 UTC+8 偏移的应用,在伦敦/纽约用户身上会算错 1 小时;用 IANA 时区名交给 intl 处理是唯一正确解;
  • 真实产品扩展:会议应存 startTime(UTC) + duration,参会人加"我的时区"字段;日历接入可用 @ohos.calendarManager
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐