欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。


在移动应用开发领域,跨端技术一直是提升开发效率、降低多端维护成本的核心方向。本文将以一个基于 React Native 开发的弹性工作制打卡应用为例,深入解析其核心实现逻辑,并探讨该应用向鸿蒙(HarmonyOS)生态跨端适配的技术路径与关键要点。

1. 类型定义

该弹性打卡应用采用 React Native 经典的函数式组件+Hooks 架构,完全遵循 React 组件化开发思想。首先通过 TypeScript 类型定义规范了核心业务数据结构:

// 员工类型定义,约束弹性工时核心属性
type Employee = {
  id: string;
  name: string;
  department: string;
  position: string;
  flexibleHours: boolean; // 是否启用弹性工时
  coreHoursStart: string; // 核心工作时段开始
  coreHoursEnd: string; // 核心工作时段结束
};

// 弹性打卡记录类型,关联员工ID与工时计算
type FlexibleWorkRecord = {
  id: string;
  employeeId: string;
  date: string;
  checkInTime: string;
  checkOutTime: string | null;
  workHours: number; // 自动计算的工作时长
};

这种强类型定义不仅提升了代码的可维护性,也为后续跨端适配时的数据结构一致性提供了基础保障。

2. 业务逻辑

应用核心状态通过 useState 管理,涵盖员工列表、打卡记录、选中员工、表单数据、模态框状态等关键数据:

  • employees:存储员工基础信息,初始化两条测试数据,标记是否启用弹性工时及核心时段
  • flexibleWorkRecords:存储所有打卡记录,支持签到、签退、工时计算等操作
  • selectedEmployee:跟踪当前选中的打卡员工,作为后续打卡操作的关联标识
  • newFlexibleRecord:管理打卡表单输入数据,包括日期、签到时间等

核心业务逻辑中,useEffect 实现了一个定时任务(每分钟),模拟生成随机员工的打卡记录,其核心在于工时计算逻辑:

const workHours = (new Date(`1970-01-01T${checkOutTime}`) - new Date(`1970-01-01T${checkInTime}`)) / (1000 * 60 * 60);

通过将时间字符串转换为 Date 对象计算时间差,再转换为小时数,这是 React Native 中处理时间计算的典型方式,该逻辑在跨端时可直接复用,仅需适配不同平台的时间格式化API差异。

3. UI 组件

应用 UI 完全基于 React Native 内置组件构建,遵循移动端原生交互体验:

  • SafeAreaView:适配刘海屏、全面屏等不同屏幕尺寸,保证内容显示在安全区域
  • ScrollView:处理长列表滚动,适配不同屏幕高度的内容展示
  • TouchableOpacity:实现可点击的员工卡片、打卡按钮等交互元素,提供原生级别的点击反馈
  • TextInput:处理打卡日期和时间的输入,绑定状态实现表单数据双向绑定
  • Modal:实现打卡记录详情的弹窗展示,支持滑动动画和半透明背景

样式方面通过 StyleSheet.create 定义,采用 Flex 布局实现响应式设计,例如:

container: {
  flex: 1,
  backgroundColor: '#f0f9ff',
},
card: {
  flexDirection: 'row',
  alignItems: 'center',
  backgroundColor: '#f0f9ff',
  borderRadius: 12,
  padding: 16,
  marginBottom: 12,
},

Flex 布局是跨端适配的核心,React Native 的 Flex 布局与鸿蒙的 Flex 布局语法高度一致,这为跨端样式迁移提供了便利。


1. 跨端方案选择

针对该应用的跨端需求,有两种主流技术路径:

(1)基于 ArkTS + React Native 桥接

鸿蒙系统提供了对 React Native 的适配能力,可通过鸿蒙的 React Native 适配层(HarmonyOS React Native)实现应用的直接迁移:

  • 核心逻辑:保留原有 React Native 代码,通过鸿蒙提供的 JS 引擎和组件映射层,将 React Native 组件转换为鸿蒙原生组件
  • 适配要点:
    • 安装鸿蒙 React Native 适配包 @hmscore/react-native-hms
    • 替换部分原生模块调用,如 Dimensions 可替换为鸿蒙的 window 模块
    • 调整样式适配鸿蒙的单位体系(如 px 与 vp 的转换)
(2)基于跨端框架重构(如 UniApp/Remax)

若需深度适配鸿蒙生态,可基于 UniApp 或 Remax 等支持鸿蒙的跨端框架重构:

  • 优势:可同时输出 React Native(iOS/Android)和鸿蒙应用,统一代码基座
  • 适配成本:需将 React Native 组件替换为框架内置组件,如 View/Text 等基础组件可直接复用,Modal/Alert 等交互组件需适配框架API

(1)数据层复用

应用的核心业务逻辑(如工时计算、状态管理、数据处理)完全基于 JavaScript/TypeScript 实现,这部分代码可 100% 复用,无需修改。只需将 React Native 特有的 API 替换为鸿蒙兼容的 API:

// React Native 原代码
const { width, height } = Dimensions.get('window');

// 鸿蒙适配后
import { window } from '@kit.ArkUI';
const width = window.getWindowProperties().windowRect.width;
const height = window.getWindowProperties().windowRect.height;
(2)UI 组件

鸿蒙的 ArkTS 提供了与 React Native 组件对应的原生组件,映射关系如下:

React Native 组件 鸿蒙 ArkTS 组件 适配要点
View Column/Row Flex 布局语法一致,直接迁移样式
Text Text 文本样式属性基本一致
TouchableOpacity Button/Text 需手动实现点击反馈效果
ScrollView List 鸿蒙 List 组件性能更优,需调整数据源绑定方式
Modal Dialog 鸿蒙 Dialog 组件需通过控制器管理显隐

以员工卡片为例,React Native 代码迁移到鸿蒙的实现:

// React Native 原代码
<TouchableOpacity 
  key={employee.id}
  style={[styles.card, selectedEmployee === employee.id && styles.selectedCard]}
  onPress={() => handleSelectEmployee(employee.id)}
>
  <Text style={styles.icon}>👤</Text>
  <View style={styles.cardInfo}>
    <Text style={styles.cardTitle}>{employee.name}</Text>
    <Text style={styles.cardDescription}>部门: {employee.department}</Text>
  </View>
</TouchableOpacity>

// 鸿蒙 ArkTS 适配后
@Builder
EmployeeCard(employee: Employee) {
  Column() {
    Row() {
      Text('👤')
        .fontSize(28)
        .marginRight(12)
      Column() {
        Text(employee.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
        Text(`部门: ${employee.department}`)
          .fontSize(14)
      }
    }
    .backgroundColor('#f0f9ff')
    .borderRadius(12)
    .padding(16)
    .marginBottom(12)
    .borderWidth(this.selectedEmployee === employee.id ? 2 : 0)
    .borderColor('#0284c7')
    .onClick(() => this.handleSelectEmployee(employee.id))
  }
}
(3)交互逻辑

鸿蒙的事件处理机制与 React Native 略有差异,但核心逻辑可复用:

  • React Native 的 onPress 对应鸿蒙的 onClick
  • React Native 的 Alert.alert 对应鸿蒙的 prompt.showToastdialog.show
  • React Native 的状态更新(setState)对应鸿蒙的 @State 装饰器

例如,签到按钮的点击逻辑迁移:

// React Native 原代码
const handleCheckIn = () => {
  if (newFlexibleRecord.date && newFlexibleRecord.checkInTime && selectedEmployee) {
    // 签到逻辑
    Alert.alert('签到成功', '新的弹性打卡记录已添加');
  } else {
    Alert.alert('提示', '请选择员工并填写完整的签到信息');
  }
};

// 鸿蒙适配后
handleCheckIn() {
  if (this.newFlexibleRecord.date && this.newFlexibleRecord.checkInTime && this.selectedEmployee) {
    // 复用相同的签到逻辑
    prompt.showToast({
      message: '签到成功,新的弹性打卡记录已添加',
      duration: 2000
    });
  } else {
    prompt.showToast({
      message: '请选择员工并填写完整的签到信息',
      duration: 2000
    });
  }
}

3. 原生能力集成

鸿蒙跨端适配后,可利用鸿蒙的原生能力提升应用性能:

  • 替换 ScrollView 为鸿蒙的 LazyForEach 实现列表懒加载,提升长列表性能
  • 集成鸿蒙的生物识别能力(如指纹、人脸),增强打卡安全性
  • 利用鸿蒙的分布式数据管理能力,实现多设备间的打卡数据同步
  • 适配鸿蒙的深色模式、系统字体大小等系统级设置,提升用户体验

该弹性工作制打卡应用的跨端适配,核心在于逻辑层复用、UI层适配、交互层迁移

  1. 业务逻辑(如工时计算、数据处理)完全基于 TypeScript 实现,可直接复用
  2. UI 组件需根据鸿蒙的组件体系进行映射替换,利用 Flex 布局减少样式调整成本
  3. 交互逻辑需适配鸿蒙的事件处理和弹窗系统,保持用户体验一致性
  4. 原生模块调用(如设备信息、时间处理)需替换为鸿蒙的对应 API

在实际项目中,建议采用渐进式适配策略:先通过 React Native 鸿蒙适配层实现快速迁移,验证核心功能;再逐步替换为鸿蒙原生组件,利用鸿蒙的原生能力提升应用性能和用户体验。这种方式既能保证跨端效率,又能充分利用鸿蒙生态的优势,是 React Native 应用向鸿蒙跨端的最优路径。

  1. 该 React Native 弹性打卡应用核心逻辑基于 TypeScript 实现,采用 Hooks 管理状态,Flex 布局实现响应式 UI,具备良好的跨端复用基础。
  2. React Native 向鸿蒙跨端可选择桥接适配或跨端框架重构两种路径,核心业务逻辑可 100% 复用,仅需适配 UI 组件和原生 API。
  3. 跨端适配的关键在于保持数据结构和业务逻辑的一致性,针对不同平台的特性调整 UI 组件和交互方式,兼顾跨端效率和原生体验。

在现代企业管理中,弹性工作制越来越受到青睐,如何有效管理弹性工作制下的员工考勤成为企业面临的新挑战。本文将深入剖析一个基于 React Native 构建的弹性工作制打卡管理应用,探讨其技术实现细节及鸿蒙跨端能力的应用。

技术选型

该应用采用了现代 React Native 函数式组件架构,通过 TypeScript 类型系统和 React Hooks 实现了一个功能完整的弹性工作制考勤管理系统。核心技术栈包括:

  • React Native:作为跨端开发框架,提供了统一的组件 API,确保应用在 iOS、Android 及鸿蒙平台上的一致性体验
  • TypeScript:通过严格的类型定义增强代码可维护性,明确了数据结构和组件接口
  • React Hooks:使用 useState 管理应用状态,useEffect 处理副作用逻辑,实现了声明式的状态管理
  • Base64 图标:采用 Base64 编码的图标资源,避免了不同平台资源格式的差异,提高了跨端兼容性
  • 响应式布局:使用 Dimensions API 获取屏幕尺寸,实现适配不同设备的响应式界面

数据模型

应用通过 TypeScript 接口定义了两个核心数据类型,构建了完整的弹性工作制考勤数据模型体系:

// 员工类型
type Employee = {
  id: string;
  name: string;
  department: string;
  position: string;
  flexibleHours: boolean;
  coreHoursStart: string;
  coreHoursEnd: string;
};

// 弹性打卡记录类型
type FlexibleWorkRecord = {
  id: string;
  employeeId: string;
  date: string;
  checkInTime: string;
  checkOutTime: string | null;
  workHours: number;
};

这种强类型设计不仅提高了代码可读性,也为鸿蒙跨端适配提供了清晰的数据契约,确保不同平台间数据传递的一致性。数据模型的设计充分考虑了弹性工作制的特点,包含了核心工作时间、弹性工作时间等关键配置,为企业管理弹性工作制提供了全面的数据支持。

状态管理

应用使用 useState Hook 管理多个复杂状态,包括员工列表、弹性打卡记录、选中状态等:

const [employees] = useState<Employee[]>([
  {
    id: '1',
    name: '李先生',
    department: '技术部',
    position: '前端工程师',
    flexibleHours: true,
    coreHoursStart: '10:00',
    coreHoursEnd: '16:00'
  },
  // 其他员工...
]);

// 其他状态定义...

特别值得注意的是,应用通过 useEffect 实现了弹性工时的自动计算机制:

// 自动计算弹性工时
useEffect(() => {
  const interval = setInterval(() => {
    const randomEmployee = employees[Math.floor(Math.random() * employees.length)];
    const checkInTime = '09:00';
    const checkOutTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    const workHours = (new Date(`1970-01-01T${checkOutTime}`) - new Date(`1970-01-01T${checkInTime}`)) / (1000 * 60 * 60);
    const newRecord: FlexibleWorkRecord = {
      id: (flexibleWorkRecords.length + 1).toString(),
      employeeId: randomEmployee.id,
      date: new Date().toISOString().split('T')[0],
      checkInTime,
      checkOutTime,
      workHours
    };
    setFlexibleWorkRecords([...flexibleWorkRecords, newRecord]);
  }, 60000);

  return () => clearInterval(interval);
}, [employees, flexibleWorkRecords]);

这种基于时间间隔的自动计算机制,模拟了真实场景中弹性工作制下的考勤管理过程,为企业提供了自动化的技术支持。同时,通过 useEffect 的清理函数,确保了定时器在组件卸载时被正确清除,避免了内存泄漏。


在 React Native 鸿蒙跨端开发中,该应用体现了以下关键技术点:

  1. 组件兼容性:使用 React Native 核心组件(如 SafeAreaView、View、Text、TouchableOpacity、ScrollView、Modal 等),确保在鸿蒙系统上的兼容性
  2. 资源管理:通过 Base64 编码的图标资源,避免了不同平台资源格式的差异,提高了跨端部署的一致性
  3. 尺寸适配:使用 Dimensions API 获取屏幕尺寸,实现响应式布局,适应不同设备屏幕
  4. 状态管理:采用 React Hooks 进行状态管理,保持跨平台代码一致性
  5. 类型安全:TypeScript 类型定义确保了数据结构在不同平台间的一致性
  6. API 调用:使用 React Native 统一的 API 调用方式,如 Alert 组件,确保在鸿蒙平台上的正确显示
  7. 中文命名支持:代码中使用了中文变量名和类型名,展示了 React Native 对中文命名的良好支持,这在鸿蒙等中文生态系统中尤为重要

弹性工作制管理

应用通过 Employee 类型中的 flexibleHours、coreHoursStart 和 coreHoursEnd 字段实现了弹性工作制的配置,为不同员工设置不同的核心工作时间,满足企业对弹性工作制的多样化需求。

工时自动计算

应用实现了工时的自动计算功能,通过计算签到和签退时间的差值,自动得出工作时长:

const workHours = (new Date(`1970-01-01T${checkOutTime}`) - new Date(`1970-01-01T${record.checkInTime}`)) / (1000 * 60 * 60);

打卡管理

应用实现了完整的弹性打卡流程,包括签到和签退功能:

// 签到功能
const handleCheckIn = () => {
  if (newFlexibleRecord.date && newFlexibleRecord.checkInTime && selectedEmployee) {
    const newRecord: FlexibleWorkRecord = {
      id: (flexibleWorkRecords.length + 1).toString(),
      employeeId: selectedEmployee,
      date: newFlexibleRecord.date,
      checkInTime: newFlexibleRecord.checkInTime,
      checkOutTime: null,
      workHours: 0
    };
    setFlexibleWorkRecords([...flexibleWorkRecords, newRecord]);
    setNewFlexibleRecord({ date: '', checkInTime: '', checkOutTime: '' });
    Alert.alert('签到成功', '新的弹性打卡记录已添加');
  } else {
    Alert.alert('提示', '请选择员工并填写完整的签到信息');
  }
};

// 签退功能
const handleCheckOut = (recordId: string) => {
  const updatedRecords = flexibleWorkRecords.map(record => {
    if (record.id === recordId) {
      const checkOutTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
      const workHours = (new Date(`1970-01-01T${checkOutTime}`) - new Date(`1970-01-01T${record.checkInTime}`)) / (1000 * 60 * 60);
      return { ...record, checkOutTime, workHours };
    }
    return record;
  });
  setFlexibleWorkRecords(updatedRecords);
  Alert.alert('签退成功', '弹性打卡记录已更新');
};

记录查看

应用提供了弹性打卡记录的查看功能,通过模态框展示详细信息:

const handleViewRecord = (recordId: string) => {
  const record = flexibleWorkRecords.find(r => r.id === recordId);
  if (record) {
    const employee = employees.find(e => e.id === record.employeeId);
    setModalContent(`员工: ${employee?.name}\n部门: ${employee?.department}\n职位: ${employee?.position}\n日期: ${record.date}\n签到时间: ${record.checkInTime}\n签退时间: ${record.checkOutTime || '未签退'}\n工作时长: ${record.workHours}小时`);
    setIsModalVisible(true);
  }
};

应用的 UI 设计遵循了现代移动应用的设计原则,使用了以下组件和交互模式:

  • 安全区域:通过 SafeAreaView 确保内容显示在安全区域内,适应不同设备的屏幕刘海和底部指示条
  • 滚动视图:通过 ScrollView 实现内容的垂直滚动,适应不同长度的员工列表和打卡记录
  • 卡片布局:使用 TouchableOpacity 和 View 组合实现卡片式列表项,提供清晰的视觉层次和交互反馈
  • 表单输入:通过 TextInput 组件实现打卡信息的输入
  • 模态框:通过 Modal 组件展示详细信息,如打卡记录详情
  • 交互反馈:使用 Alert 组件提供操作反馈和提示信息
  • 响应式设计:根据屏幕尺寸动态调整布局,确保在不同设备上的良好显示效果

  1. 跨端架构:基于 React Native 构建,实现了一次编码多平台运行的目标,特别关注了鸿蒙平台的适配
  2. 类型安全:全面使用 TypeScript 类型定义,提高代码质量和可维护性,确保考勤数据的准确性
  3. 弹性工作制支持:通过核心工作时间配置,实现了对弹性工作制的有效管理
  4. 自动化工时计算:通过定时任务自动计算工时,提高了考勤管理的效率
  5. 智能状态管理:通过 React Hooks 实现了简洁的状态管理,提高了代码的可读性和可维护性
  6. 模块化设计:通过清晰的类型定义和函数划分,实现了代码的模块化,提高了可维护性
  7. 实时数据反馈:通过即时的 Alert 反馈,增强用户操作体验
  8. 数据结构设计:通过关联的数据结构,如弹性打卡记录关联员工,实现了复杂考勤数据的有效组织
  9. 中文命名支持:代码中使用了中文变量名和类型名,展示了 React Native 对中文命名的良好支持,这在鸿蒙等中文生态系统中尤为重要
  10. 灵活性:支持不同员工的个性化核心工作时间配置,满足企业多样化的弹性工作制需求


在实际应用中,还可以考虑以下性能优化策略:

  1. 状态管理优化:对于大型应用,可以考虑使用 Redux 或 Context API 进行全局状态管理,提高状态更新的效率
  2. 组件拆分:将大型组件拆分为更小的可复用组件,提高渲染性能和代码可维护性
  3. 数据缓存:对员工数据和弹性打卡记录进行本地缓存,减少重复计算和网络请求
  4. 动画性能:使用 React Native 的 Animated API 实现流畅的过渡动画,提升用户体验
  5. 内存管理:确保及时清理不再使用的状态和事件监听器,避免内存泄漏
  6. 网络优化:对于实际应用中的远程数据同步,实现合理的网络请求策略,如批量上传、增量同步等
  7. 计算优化:对于工时计算等操作,可以考虑使用 memoization 技术缓存计算结果
  8. 列表优化:对于长列表,使用 FlatList 组件替代 ScrollView,提高渲染性能

在开发过程中,可能面临的技术挑战及解决方案:

  1. 鸿蒙平台适配:通过使用 React Native 核心组件和统一的 API 调用方式,确保应用在鸿蒙系统上的兼容性
  2. 实时数据同步:在实际应用中,可以实现与后端服务器的实时数据同步,确保弹性打卡数据的一致性
  3. 弹性工时规则复杂化:可以实现更复杂的弹性工时规则,如不同部门、不同岗位的差异化弹性工作时间
  4. 数据安全:实现弹性打卡数据的加密存储和传输,保护企业数据安全
  5. 离线功能:实现基本的离线操作能力,确保在网络不稳定情况下的正常使用
  6. 性能优化:针对不同设备性能差异,实现自适应的性能优化策略,确保在中低端设备上的流畅运行
  7. 用户体验一致性:确保在不同平台上的用户体验一致,特别是交互方式和视觉效果
  8. 多语言支持:实现多语言支持,满足不同地区企业的需求

通过对这个弹性工作制打卡管理应用的技术解读,我们可以看到 React Native 在跨端开发中的强大能力。该应用不仅实现了完整的弹性工作制考勤管理功能,还展示了如何通过 TypeScript、React Hooks 等现代前端技术构建高质量的跨端应用。


真实演示案例代码:







// App.tsx
import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, TextInput, Modal } from 'react-native';

// Base64 图标库
const ICONS_BASE64 = {
  弹性: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  工时: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  用户: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  时间: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};

const { width, height } = Dimensions.get('window');

// 员工类型
type Employee = {
  id: string;
  name: string;
  department: string;
  position: string;
  flexibleHours: boolean;
  coreHoursStart: string;
  coreHoursEnd: string;
};

// 弹性打卡记录类型
type FlexibleWorkRecord = {
  id: string;
  employeeId: string;
  date: string;
  checkInTime: string;
  checkOutTime: string | null;
  workHours: number;
};

// 弹性工作制打卡管理应用组件
const FlexibleWorkAttendanceApp: React.FC = () => {
  const [employees] = useState<Employee[]>([
    {
      id: '1',
      name: '李先生',
      department: '技术部',
      position: '前端工程师',
      flexibleHours: true,
      coreHoursStart: '10:00',
      coreHoursEnd: '16:00'
    },
    {
      id: '2',
      name: '王女士',
      department: '市场部',
      position: '市场专员',
      flexibleHours: true,
      coreHoursStart: '09:30',
      coreHoursEnd: '17:30'
    }
  ]);

  const [flexibleWorkRecords, setFlexibleWorkRecords] = useState<FlexibleWorkRecord[]>([
    {
      id: '1',
      employeeId: '1',
      date: '2023-12-01',
      checkInTime: '09:00',
      checkOutTime: '18:00',
      workHours: 8
    }
  ]);

  const [selectedEmployee, setSelectedEmployee] = useState<string | null>(null);
  const [newFlexibleRecord, setNewFlexibleRecord] = useState({
    date: '',
    checkInTime: '',
    checkOutTime: ''
  });
  const [isModalVisible, setIsModalVisible] = useState(false);
  const [modalContent, setModalContent] = useState('');

  // 自动计算弹性工时
  useEffect(() => {
    const interval = setInterval(() => {
      const randomEmployee = employees[Math.floor(Math.random() * employees.length)];
      const checkInTime = '09:00';
      const checkOutTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
      const workHours = (new Date(`1970-01-01T${checkOutTime}`) - new Date(`1970-01-01T${checkInTime}`)) / (1000 * 60 * 60);
      const newRecord: FlexibleWorkRecord = {
        id: (flexibleWorkRecords.length + 1).toString(),
        employeeId: randomEmployee.id,
        date: new Date().toISOString().split('T')[0],
        checkInTime,
        checkOutTime,
        workHours
      };
      setFlexibleWorkRecords([...flexibleWorkRecords, newRecord]);
    }, 60000);

    return () => clearInterval(interval);
  }, [employees, flexibleWorkRecords]);

  const handleSelectEmployee = (employeeId: string) => {
    setSelectedEmployee(employeeId);
    Alert.alert('选择员工', '您已选择该员工进行弹性打卡');
  };

  const handleCheckIn = () => {
    if (newFlexibleRecord.date && newFlexibleRecord.checkInTime && selectedEmployee) {
      const newRecord: FlexibleWorkRecord = {
        id: (flexibleWorkRecords.length + 1).toString(),
        employeeId: selectedEmployee,
        date: newFlexibleRecord.date,
        checkInTime: newFlexibleRecord.checkInTime,
        checkOutTime: null,
        workHours: 0
      };
      setFlexibleWorkRecords([...flexibleWorkRecords, newRecord]);
      setNewFlexibleRecord({ date: '', checkInTime: '', checkOutTime: '' });
      Alert.alert('签到成功', '新的弹性打卡记录已添加');
    } else {
      Alert.alert('提示', '请选择员工并填写完整的签到信息');
    }
  };

  const handleCheckOut = (recordId: string) => {
    const updatedRecords = flexibleWorkRecords.map(record => {
      if (record.id === recordId) {
        const checkOutTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
        const workHours = (new Date(`1970-01-01T${checkOutTime}`) - new Date(`1970-01-01T${record.checkInTime}`)) / (1000 * 60 * 60);
        return { ...record, checkOutTime, workHours };
      }
      return record;
    });
    setFlexibleWorkRecords(updatedRecords);
    Alert.alert('签退成功', '弹性打卡记录已更新');
  };

  const handleViewRecord = (recordId: string) => {
    const record = flexibleWorkRecords.find(r => r.id === recordId);
    if (record) {
      const employee = employees.find(e => e.id === record.employeeId);
      setModalContent(`员工: ${employee?.name}\n部门: ${employee?.department}\n职位: ${employee?.position}\n日期: ${record.date}\n签到时间: ${record.checkInTime}\n签退时间: ${record.checkOutTime || '未签退'}\n工作时长: ${record.workHours}小时`);
      setIsModalVisible(true);
    }
  };

  const openModal = (content: string) => {
    setModalContent(content);
    setIsModalVisible(true);
  };

  const closeModal = () => {
    setIsModalVisible(false);
  };

  return (
    <SafeAreaView style={styles.container}>
      {/* 头部 */}
      <View style={styles.header}>
        <Text style={styles.title}>弹性工作制打卡</Text>
        <Text style={styles.subtitle}>支持弹性工作制员工的打卡管理,系统根据弹性规则计算工时</Text>
      </View>

      <ScrollView style={styles.content}>
        {/* 员工列表 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>员工列表</Text>
          {employees.map(employee => (
            <TouchableOpacity 
              key={employee.id}
              style={[
                styles.card,
                selectedEmployee === employee.id && styles.selectedCard
              ]}
              onPress={() => handleSelectEmployee(employee.id)}
            >
              <Text style={styles.icon}>👤</Text>
              <View style={styles.cardInfo}>
                <Text style={styles.cardTitle}>{employee.name}</Text>
                <Text style={styles.cardDescription}>部门: {employee.department}</Text>
                <Text style={styles.cardDescription}>职位: {employee.position}</Text>
                <Text style={styles.cardDescription}>弹性工时: {employee.flexibleHours ? '是' : '否'}</Text>
                <Text style={styles.cardDescription}>核心时段: {employee.coreHoursStart} - {employee.coreHoursEnd}</Text>
              </View>
            </TouchableOpacity>
          ))}
        </View>

        {/* 弹性打卡 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>弹性打卡</Text>
          <View style={styles.inputRow}>
            <TextInput
              style={styles.input}
              placeholder="打卡日期 (YYYY-MM-DD)"
              value={newFlexibleRecord.date}
              onChangeText={(text) => setNewFlexibleRecord({ ...newFlexibleRecord, date: text })}
            />
            <TextInput
              style={styles.input}
              placeholder="签到时间 (HH:MM)"
              value={newFlexibleRecord.checkInTime}
              onChangeText={(text) => setNewFlexibleRecord({ ...newFlexibleRecord, checkInTime: text })}
            />
          </View>
          <TouchableOpacity 
            style={styles.addButton}
            onPress={handleCheckIn}
          >
            <Text style={styles.addText}>弹性签到</Text>
          </TouchableOpacity>
        </View>

        {/* 弹性打卡记录 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>弹性打卡记录</Text>
          {flexibleWorkRecords.map(record => (
            <TouchableOpacity 
              key={record.id}
              style={styles.recordCard}
              onPress={() => handleViewRecord(record.id)}
            >
              <Text style={styles.icon}></Text>
              <View style={styles.cardInfo}>
                <Text style={styles.cardTitle}>记录ID: {record.id}</Text>
                <Text style={styles.cardDescription}>日期: {record.date}</Text>
                <Text style={styles.cardDescription}>签到时间: {record.checkInTime}</Text>
                <Text style={styles.cardDescription}>签退时间: {record.checkOutTime || '未签退'}</Text>
                <Text style={styles.cardDescription}>工作时长: {record.workHours}小时</Text>
              </View>
              {!record.checkOutTime && (
                <TouchableOpacity 
                  style={styles.checkOutButton}
                  onPress={() => handleCheckOut(record.id)}
                >
                  <Text style={styles.checkOutText}>签退</Text>
                </TouchableOpacity>
              )}
            </TouchableOpacity>
          ))}
        </View>

        {/* 使用说明 */}
        <View style={styles.infoCard}>
          <Text style={styles.sectionTitle}>📘 使用说明</Text>
          <Text style={styles.infoText}>• 选择员工进行弹性打卡</Text>
          <Text style={styles.infoText}>• 填写打卡日期和时间</Text>
          <Text style={styles.infoText}>• 系统根据弹性规则自动计算工时</Text>
          <Text style={styles.infoText}>• 查看历史弹性打卡记录</Text>
        </View>

        {/* 弹框内容 */}
        <Modal
          animationType="slide"
          transparent={true}
          visible={isModalVisible}
          onRequestClose={closeModal}
        >
          <View style={styles.modalContainer}>
            <View style={styles.modalContent}>
              <Text style={styles.modalTitle}>详细信息</Text>
              <Text style={styles.modalText}>{modalContent}</Text>
              <TouchableOpacity
                style={styles.closeButton}
                onPress={closeModal}
              >
                <Text style={styles.closeButtonText}>关闭</Text>
              </TouchableOpacity>
            </View>
          </View>
        </Modal>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f0f9ff',
  },
  header: {
    flexDirection: 'column',
    padding: 16,
    backgroundColor: '#ffffff',
    borderBottomWidth: 1,
    borderBottomColor: '#bae6fd',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#0c4a6e',
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 14,
    color: '#0284c7',
  },
  content: {
    flex: 1,
    marginTop: 12,
  },
  section: {
    backgroundColor: '#ffffff',
    marginHorizontal: 16,
    marginBottom: 12,
    borderRadius: 12,
    padding: 16,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#0c4a6e',
    marginBottom: 12,
  },
  card: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f0f9ff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
  },
  selectedCard: {
    borderWidth: 2,
    borderColor: '#0284c7',
  },
  recordCard: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f0f9ff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
  },
  icon: {
    fontSize: 28,
    marginRight: 12,
  },
  cardInfo: {
    flex: 1,
  },
  cardTitle: {
    fontSize: 16,
    fontWeight: '500',
    color: '#0c4a6e',
    marginBottom: 4,
  },
  cardDescription: {
    fontSize: 14,
    color: '#0284c7',
    marginBottom: 2,
  },
  inputRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 12,
  },
  input: {
    flex: 1,
    backgroundColor: '#f0f9ff',
    borderRadius: 8,
    paddingHorizontal: 12,
    paddingVertical: 8,
    fontSize: 14,
    color: '#0c4a6e',
    marginRight: 8,
  },
  addButton: {
    backgroundColor: '#0284c7',
    padding: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  addText: {
    color: '#ffffff',
    fontSize: 14,
    fontWeight: '500',
  },
  checkOutButton: {
    backgroundColor: '#10b981',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 8,
  },
  checkOutText: {
    color: '#ffffff',
    fontSize: 12,
    fontWeight: '500',
  },
  infoCard: {
    backgroundColor: '#ffffff',
    marginHorizontal: 16,
    marginBottom: 80,
    borderRadius: 12,
    padding: 16,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  infoText: {
    fontSize: 14,
    color: '#64748b',
    lineHeight: 20,
    marginBottom: 4,
  },
  modalContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
  },
  modalContent: {
    width: '80%',
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 20,
    elevation: 5,
  },
  modalTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#0c4a6e',
    marginBottom: 12,
    textAlign: 'center',
  },
  modalText: {
    fontSize: 14,
    color: '#0c4a6e',
    lineHeight: 20,
    marginBottom: 20,
  },
  closeButton: {
    backgroundColor: '#0284c7',
    padding: 10,
    borderRadius: 8,
    alignItems: 'center',
  },
  closeButtonText: {
    color: '#ffffff',
    fontSize: 14,
    fontWeight: '500',
  },
});

export default FlexibleWorkAttendanceApp;

请添加图片描述


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

在这里插入图片描述

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

在这里插入图片描述

最后运行效果图如下显示:

请添加图片描述
摘要
本文以React Native开发的弹性工作制打卡应用为例,探讨了向鸿蒙生态跨端适配的技术路径。文章首先介绍了应用的TypeScript类型定义和核心业务逻辑,包括员工数据管理和工时计算功能。在UI组件方面,应用采用React Native原生组件构建,使用Flex布局实现响应式设计。针对跨端需求,提出了两种适配方案:基于ArkTS的React Native桥接和跨端框架重构,并详细对比了组件映射关系和交互逻辑差异。文章重点分析了数据层复用、UI组件转换和交互逻辑适配等关键环节,为开发者提供了实用的跨端迁移指南。

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

Logo

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

更多推荐