基于React Native鸿蒙跨平台通过map遍历+不可变更新的方式修改指定记录的endTime,适配鸿蒙多设备间的状态同步要求
本文介绍了一款基于React Native和鸿蒙系统的教师授课打卡应用,通过跨平台技术实现高校教学管理的数字化。应用采用React Native通用API构建核心功能,适配鸿蒙ArkUI原生组件,保留原生交互体验。通过TypeScript构建强类型数据模型,确保跨端数据一致性。利用React Hooks管理状态,实现授课记录的自动生成和手动操作,适配多终端使用场景。该方案充分发挥鸿蒙分布式能力与R
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
在高校数字化教学管理体系中,教师授课打卡是教学过程管控的核心环节,其核心诉求在于教师-教室的精准关联、授课时段的完整记录、跨终端数据同步的一致性。鸿蒙系统的分布式数据管理与全场景设备协同能力,为授课打卡这类多角色(教师/教学管理员)、多终端(手机/平板/智慧屏)协作的教育管理场景提供了天然的技术优势;而React Native凭借“一次开发、多端运行”的特性,成为教师授课打卡应用跨端开发的最优选择。本文将从高校教学场景专属的数据模型设计、教师-教室联动打卡逻辑、授课时段状态流转机制到鸿蒙生态融合的底层实现,全方位拆解这款教师授课打卡应用的技术架构,剖析React Native与鸿蒙生态深度融合的关键技术要点。
保留React Native的点击反馈特性
这款教师授课打卡应用的核心架构完全基于React Native通用API体系构建,未引入任何平台专属代码,这是实现鸿蒙跨端兼容的核心前提。React Native for HarmonyOS框架会将React Native的通用组件无缝映射为鸿蒙ArkUI原生组件,保证教师/教室选择、授课打卡、记录查看等核心交互在鸿蒙设备上的原生质感,同时适配高校教学管理场景下多终端使用的特性。
从组件适配层面来看,TouchableOpacity作为核心交互载体,在鸿蒙系统中被映射为Button原生组件——教师列表卡片、教室列表卡片的点击选择逻辑,“开始授课”“结束授课”按钮的触发操作,均保留React Native的点击反馈特性,同时适配鸿蒙系统的交互规范;尤其是选中态的selectedCard样式(2px蓝色边框),在鸿蒙系统中渲染为符合原生设计规范的选中反馈,避免因跨端视觉差异导致的选错教师/教室问题,契合授课打卡“人员-场地关联精准”的核心诉求。Alert组件在鸿蒙系统中调用系统级弹窗能力,无论是选择教师/教室后的确认提示、开始/结束授课的状态反馈,都能直接调用鸿蒙的通知栏能力,实现“应用内弹窗+系统通知”的双重反馈,适配教学管理场景下教师快速确认操作状态的需求。
Modal组件对应鸿蒙Dialog原生模态框,通过animationType="slide"实现的滑动弹窗效果,在鸿蒙系统中解析为原生动效,用于展示包含教师职称、教室容量、授课时段的完整详情,符合鸿蒙系统的交互体验标准;TextInput作为授课信息录入组件,在鸿蒙系统中适配TextInput原生控件的输入规则,支持日期(YYYY-MM-DD)、时间(HH:MM)等结构化数据的精准录入,避免跨端输入格式不兼容的问题,适配授课打卡“时间记录精准”的核心要求。
此外,应用通过Dimensions.get('window')获取设备屏幕尺寸,该API在鸿蒙系统中被适配为getWindowSize原生能力,能够精准识别鸿蒙手机(教师移动端)、平板(教学管理员端)、智慧屏(教学楼管理大屏)等不同形态设备的屏幕参数——例如在鸿蒙平板上管理员可批量查看全院教师授课打卡状态,在手机上教师仅能完成个人授课打卡操作,在智慧屏上可展示各教室授课占用实时状态,完美契合高校教学管理场景下多角色、多终端的使用需求。
适配鸿蒙ArkTS的静态类型特性
教师授课打卡的核心是“教师-教室-授课记录”的三层关联,代码中通过TypeScript构建了高校教学场景专属的强类型数据模型,尤其是通过teacherId和classroomId实现的跨表关联,以及endTime字段的string | null联合类型约束,既规避前端开发中的数据关联错误,又在跨端编译阶段拦截非法值,适配鸿蒙ArkTS的静态类型特性。
// 教师模型:高校教学场景专属属性,支撑授课打卡的人员精准关联
type Teacher = {
id: string;
name: string;
department: string; // 院系字段,高校教学管理核心维度,适配院系级授课统计
position: string; // 职称字段,区分教师职级,适配不同职级的授课考核规则
};
// 教室模型:教学场地核心属性,作为授课记录的关联基准
type Classroom = {
id: string;
name: string; // 教室编号,高校标准化场地标识(如A101)
location: string; // 地理位置,适配多教学楼分布的高校场景
capacity: number; // 容量字段,数值类型适配教室使用效率统计
};
// 授课打卡记录模型:关联教师与教室,实现授课时段的完整管控
type TeachingAttendanceRecord = {
id: string;
teacherId: string; // 关联教师ID,实现授课记录与教师的精准绑定
classroomId: string; // 关联教室ID,实现授课记录与场地的精准绑定
date: string; // 授课日期,兼容跨端统一的YYYY-MM-DD格式
startTime: string; // 授课开始时间,适配HH:MM格式的跨端统一解析
endTime: string | null; // 授课结束时间,null值表示未结束,适配授课状态流转
};
其中,endTime采用string | null的联合类型是教学场景的核心设计亮点:在鸿蒙系统中,React Native的TypeScript编译器会对JS层与ArkTS层之间的状态值传递进行严格校验,避免出现非法结束时间值导致的跨端数据错误;同时,null值的设计完美适配“授课中”这一中间状态,保证跨端数据的一致性,不会因鸿蒙系统的类型解析规则差异导致授课时长统计错误。capacity字段采用数值类型,区别于其他字符串型属性,适配高校教学管理中“教室容量利用率”的量化统计需求,在跨端数据同步时可直接对接教务系统的数据分析模块,提升应用的实用性。
React Hooks驱动的跨端授课打卡流程
应用的核心业务逻辑(自动生成授课记录、手动开始/结束授课、详情查看)均基于React Hooks(useState、useEffect)实现,这种轻量级状态管理方式完美适配React Native的跨端生命周期模型,同时与鸿蒙组件生命周期深度融合,保障了授课打卡流程的跨端稳定运行。
通过useState管理核心状态
应用通过useState管理核心状态,包括教师列表、教室列表、授课记录列表、选中教师/教室、新授课录入表单、弹窗状态等。其中,所有状态更新均采用“不可变更新”策略,是跨端数据兼容的关键:
// 开始授课的不可变更新
setTeachingAttendanceRecords([...teachingAttendanceRecords, newRecord]);
// 结束授课的不可变更新
const updatedRecords = teachingAttendanceRecords.map(record =>
record.id === recordId ? { ...record, endTime: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) } : record
);
setTeachingAttendanceRecords(updatedRecords);
在鸿蒙系统中,不可变更新(通过解构赋值创建新数组/对象)保证每次状态变更都会生成新的数据源,避免多端数据同步时的冲突问题——例如教师在鸿蒙手机上结束授课后,教学管理员在鸿蒙平板上能实时看到授课结束时间更新,符合授课打卡“状态实时更新、可追溯”的核心要求。同时,selectedTeacher和selectedClassroom双状态设计,适配授课打卡“必须选择教师+教室才能开始授课”的业务规则,在提交前的校验逻辑中确保关联关系的完整性,避免跨端数据关联错误。
自动生成授课
应用通过useEffect实现每分钟一次的自动生成授课记录逻辑,模拟高校教学场景下教师批量打卡的业务场景,这一核心逻辑在鸿蒙系统中稳定运行的关键在于:
useEffect(() => {
const interval = setInterval(() => {
const randomTeacher = teachers[Math.floor(Math.random() * teachers.length)];
const randomClassroom = classrooms[Math.floor(Math.random() * classrooms.length)];
const newRecord: TeachingAttendanceRecord = {
id: (teachingAttendanceRecords.length + 1).toString(),
teacherId: randomTeacher.id,
classroomId: randomClassroom.id,
date: new Date().toISOString().split('T')[0], // 跨端兼容的日期格式化
startTime: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), // 跨端统一的时间格式化
endTime: null
};
setTeachingAttendanceRecords([...teachingAttendanceRecords, newRecord]);
}, 60000);
return () => clearInterval(interval);
}, [teachers, classrooms, teachingAttendanceRecords]);
setInterval/clearInterval是React Native封装的通用定时器API,已适配鸿蒙的任务调度机制,不会因鸿蒙系统的后台任务管理策略导致定时器失效;new Date().toISOString().split('T')[0]和new Date().toLocaleTimeString()是跨端通用的日期时间格式化方法,在鸿蒙系统中解析为标准的YYYY-MM-DD和HH:MM格式,保证不同鸿蒙设备上生成的授课时间一致性;通过随机选择教师和教室生成授课记录,模拟了真实教学场景中“不同教师在不同教室授课”的核心规则,生成的记录完全符合TypeScript类型约束,避免跨端运行时错误。
useEffect的依赖数组包含teachers、classrooms和teachingAttendanceRecords,保证数据源变更时重新创建定时器,适配鸿蒙组件的更新生命周期;返回的清理函数对应鸿蒙组件onDestroy生命周期,确保组件卸载时销毁定时器,避免鸿蒙设备内存泄漏,尤其适配高校教学场景下应用频繁启停的使用特点。
教师-教室联动
handleStartClass函数是手动开始授课的核心入口,其内部首先校验“选中教师+选中教室+日期+开始时间”的完整性,这是授课打卡的核心校验逻辑,保证录入数据的合规性;随后构建符合TypeScript约束的新授课记录对象,通过不可变更新添加到状态中,确保鸿蒙分布式数据环境下的同步准确性。handleEndClass函数实现授课结束状态更新,通过map遍历+不可变更新的方式修改指定记录的endTime,保证状态变更的可追溯性,适配鸿蒙多设备间的状态同步要求。handleViewRecord函数通过teacherId和classroomId关联查询教师、教室信息,展示完整的授课详情,通过可选链操作符(teacher?.name、classroom?.capacity)处理空值情况,避免因关联ID错误导致的跨端运行时错误,符合鸿蒙系统对应用稳定性的严苛要求。
应用的UI层基于React Native的StyleSheet统一管理样式,既保证鸿蒙系统中的原生渲染效果,又兼顾教师授课打卡这类高校教学管理场景对视觉辨识度、操作便捷性的特殊要求。
样式
StyleSheet将CSS样式抽象为跨平台的样式对象,核心样式属性在鸿蒙系统中会被精准转换为ArkUI的布局属性,同时针对高校教学场景设计了差异化的视觉样式:
const styles = StyleSheet.create({
section: {
backgroundColor: '#ffffff',
marginHorizontal: 16,
borderRadius: 12,
padding: 16,
// 阴影跨端适配:elevation适配鸿蒙/Android,shadow系列适配iOS
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
marginBottom: 12,
},
card: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f0f9ff',
borderRadius: 12,
padding: 16,
marginBottom: 12,
},
selectedCard: {
borderWidth: 2,
borderColor: '#0284c7', // 统一的选中态边框,适配教师/教室选择的可视化
},
endButton: {
backgroundColor: '#10b981', // 绿色结束按钮,突出授课收尾操作
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
}
});
其中,elevation属性在鸿蒙系统中会被解析为原生阴影层级,borderRadius适配鸿蒙的圆角渲染规则,保证UI视觉效果的跨端一致性;card样式为教师/教室列表项设计了统一的布局结构(图标+核心信息),既保证高校教学场景下关键信息(院系、教室位置)的清晰展示,又适配鸿蒙系统的布局渲染规则;selectedCard样式为选中的教师/教室提供明确的视觉反馈,避免因跨端视觉差异导致的选错问题,符合授课打卡“关联精准”的核心要求;endButton采用绿色系(#10b981),与鸿蒙系统的“完成/确认”类操作按钮视觉规范一致,在跨端展示时具备高辨识度,符合授课打卡“结束操作醒目、一键完成”的核心要求。
交互组件
核心交互逻辑上,教师列表和教室列表采用相同的卡片样式和选中态设计,保证操作体验的一致性,适配高校教师/管理员的操作习惯;Modal弹窗展示授课记录的完整详情,包含教师职称、教室容量、位置等高校教学管理专属信息,满足教学管理中“信息完整可查”的需求;inputRow采用分组布局,将日期、开始时间字段分类录入,适配授课打卡“结构化时间”的录入规则,提升跨端录入效率;仅对未结束的授课记录展示“结束授课”按钮,实现基于状态的条件渲染,避免无效操作按钮的展示,符合鸿蒙系统对交互简洁性的设计规范。
当前代码已实现基础的鸿蒙跨端兼容,在生产环境中,可针对鸿蒙系统特性和高校教师授课打卡场景需求进行深度优化,进一步提升应用的教育级服务能力:
1. 鸿蒙原生
教师授课打卡的核心诉求是“多角色数据同步、多终端协同管理”,可通过React Native的Native Module机制封装鸿蒙原生分布式数据能力:
- 集成鸿蒙
DistributedDataManager,实现授课记录在教师鸿蒙手机、管理员鸿蒙平板、教学楼智慧屏之间的实时同步,确保教师完成开始/结束授课操作后,管理员端和大屏端能即时看到状态更新; - 利用鸿蒙的分布式权限管理能力,为不同角色分配差异化权限(如教师仅能查看/提交自身授课记录、管理员可查看全院授课数据、教务处可导出授课时长统计报表),适配高校教学管理的权限分级体系;
- 适配鸿蒙的分布式任务调度能力,在授课开始前自动向教师鸿蒙设备推送打卡提醒,在授课结束时间临近时提醒教师完成结束打卡,提升授课打卡的完整性。
2. 高性能列表
应用中授课记录列表采用ScrollView + map的方式渲染,在鸿蒙系统中面对全院教师的授课记录时可能出现卡顿。可替换为React Native的FlatList组件,该组件在鸿蒙系统中会适配ArkUI的List原生组件,实现按需渲染和组件复用,通过getItemLayout优化列表滚动性能,尤其适合展示全院、全学期的批量授课记录场景。
3. 高校教学
基于鸿蒙的AI能力和React Native的状态管理,可实现教师授课打卡的智能化管理:
- 通过鸿蒙的数据分析能力,自动统计教师授课时长、教室使用效率,生成院系/教师维度的授课统计报告,为教学资源调配、教师考核提供数据支撑;
- 结合鸿蒙的位置服务能力,实现授课打卡的地理位置校验(如仅在教室内可完成开始/结束授课打卡),避免代打卡等违规行为,提升打卡数据的真实性;
- 利用鸿蒙的NFC能力,实现教师刷校园卡自动完成授课打卡,适配高校校园一卡通的使用场景,提升打卡操作的便捷性。
这款基于React Native开发的教师授课打卡应用,通过高校教学场景专属的强类型数据模型、React Hooks状态管理和通用UI组件设计,构建了具备完整鸿蒙跨端兼容能力的教育管理类应用架构,核心技术要点可总结为:
- 通用API选型是实现鸿蒙兼容的基础,基于React Native通用组件构建核心逻辑,规避平台专属代码,保证了授课打卡管理UI和交互的跨端一致性,尤其是
TouchableOpacity和Alert组件适配鸿蒙原生交互规范,满足高校教学场景“操作简单、反馈及时”的核心要求; - TypeScript教学场景数据模型适配鸿蒙ArkTS的静态类型特性,通过
teacherId和classroomId实现教师-教室-授课记录的精准关联,避免跨端数据交互中的关联错误,保障授课打卡数据的精准性; - React Hooks不可变更新策略与鸿蒙组件生命周期深度融合,实现授课状态的精准更新和多端同步,保障了核心授课打卡流程的跨端稳定运行;
- 统一的StyleSheet样式系统实现了UI在鸿蒙设备上的原生渲染,差异化的选中态和结束按钮设计兼顾了高校教学场景的操作辨识度和鸿蒙系统的视觉规范。
本文将深入解析一个基于 React Native 开发的教师授课打卡系统,该系统专为教育场景设计,支持教师在授课前后进行打卡记录,并通过跨端技术实现在鸿蒙等多个平台的无缝运行。
技术栈
该应用采用了现代化的 React Native 技术栈,核心组件包括:
- React 18+:利用最新的 React 函数式组件和 Hooks API
- TypeScript:提供类型安全,增强代码可维护性
- React Native 核心组件:SafeAreaView、View、Text、TouchableOpacity 等
- 响应式布局:使用 Dimensions API 实现适配不同屏幕尺寸
鸿蒙跨端
从代码实现来看,该应用采用了多种策略确保在鸿蒙系统上的良好运行:
- Base64 图标资源:将图标编码为 Base64 格式,避免了不同平台资源文件格式差异的问题
- 平台无关的组件:使用 React Native 核心组件,确保在鸿蒙系统上的兼容性
- 响应式设计:通过 Dimensions API 获取屏幕尺寸,动态调整布局
- TypeScript 类型定义:增强代码健壮性,减少运行时错误
类型系统
应用使用 TypeScript 定义了三个核心数据模型:
// 教师类型
type Teacher = {
id: string;
name: string;
department: string;
position: string;
};
// 教室类型
type Classroom = {
id: string;
name: string;
location: string;
capacity: number;
};
// 授课打卡记录类型
type TeachingAttendanceRecord = {
id: string;
teacherId: string;
classroomId: string;
date: string;
startTime: string;
endTime: string | null;
};
这种类型定义方式不仅提高了代码可读性,也为鸿蒙跨端开发提供了更强的类型保障,减少了平台差异导致的类型错误。
状态管理
应用使用 React Hooks 进行状态管理,主要包括:
const [teachers] = useState<Teacher[]>([...]);
const [classrooms] = useState<Classroom[]>([...]);
const [teachingAttendanceRecords, setTeachingAttendanceRecords] = useState<TeachingAttendanceRecord[]>([...]);
const [selectedTeacher, setSelectedTeacher] = useState<string | null>(null);
const [selectedClassroom, setSelectedClassroom] = useState<string | null>(null);
const [newAttendanceRecord, setNewAttendanceRecord] = useState({
date: '',
startTime: ''
});
const [isModalVisible, setIsModalVisible] = useState(false);
const [modalContent, setModalContent] = useState('');
这种状态管理方式简洁明了,适合中小型应用,同时也便于在鸿蒙系统上进行适配。
自动打卡机制
应用实现了一个基于定时器的自动打卡功能,每 60 秒随机生成一条打卡记录:
useEffect(() => {
const interval = setInterval(() => {
const randomTeacher = teachers[Math.floor(Math.random() * teachers.length)];
const randomClassroom = classrooms[Math.floor(Math.random() * classrooms.length)];
const newRecord: TeachingAttendanceRecord = {
id: (teachingAttendanceRecords.length + 1).toString(),
teacherId: randomTeacher.id,
classroomId: randomClassroom.id,
date: new Date().toISOString().split('T')[0],
startTime: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
endTime: null
};
setTeachingAttendanceRecords([...teachingAttendanceRecords, newRecord]);
}, 60000);
return () => clearInterval(interval);
}, [teachers, classrooms, teachingAttendanceRecords]);
这个实现展示了如何使用 useEffect Hook 处理副作用,同时也体现了跨平台时间处理的一致性。
授课打卡流程
应用实现了完整的授课打卡流程,包括:
- 教师和教室选择:通过点击事件选择授课教师和教室
- 开始授课:记录授课开始时间
- 结束授课:记录授课结束时间
- 查看记录详情:通过 Modal 展示详细的打卡记录
这些功能的实现充分利用了 React Native 的事件处理机制和组件生命周期管理。
模态框与用户交互
应用使用 Modal 组件展示详细的打卡记录,提供了良好的用户体验:
const handleViewRecord = (recordId: string) => {
const record = teachingAttendanceRecords.find(r => r.id === recordId);
if (record) {
const teacher = teachers.find(t => t.id === record.teacherId);
const classroom = classrooms.find(c => c.id === record.classroomId);
setModalContent(`教师: ${teacher?.name}\n部门: ${teacher?.department}\n职位: ${teacher?.position}\n教室: ${classroom?.name}\n位置: ${classroom?.location}\n容量: ${classroom?.capacity}人\n日期: ${record.date}\n开始时间: ${record.startTime}\n结束时间: ${record.endTime || '未结束'}`);
setIsModalVisible(true);
}
};
这种实现方式在鸿蒙系统上也能保持一致的用户体验。
响应式布局
应用通过 Dimensions API 获取屏幕尺寸,实现了响应式布局:
const { width, height } = Dimensions.get('window');
这种方式确保了应用在不同尺寸的设备上都能提供良好的显示效果,包括鸿蒙系统的各种设备。
安全区域
应用使用 SafeAreaView 组件,确保内容不会被设备的状态栏或刘海遮挡:
<SafeAreaView style={styles.container}>
{/* 应用内容 */}
</SafeAreaView>
这是一种跨平台的适配方案,能够在鸿蒙系统上自动处理安全区域问题。
用户反馈
应用使用 Alert 组件提供用户反馈,确保操作的可见性:
Alert.alert('开始授课', '新的授课打卡记录已添加');
这种方式在鸿蒙系统上也能保持一致的用户体验。
基于代码分析,以下是一些针对鸿蒙系统的优化建议:
-
性能优化:对于自动打卡功能,可考虑使用鸿蒙系统的后台任务调度能力,减少前台应用的资源消耗
-
原生能力集成:可集成鸿蒙系统的位置服务,实现基于位置的打卡验证
-
UI 适配:可根据鸿蒙系统的设计规范,调整应用的视觉风格,提供更符合鸿蒙用户习惯的界面
-
数据持久化:可使用鸿蒙系统的分布式数据管理能力,实现多设备数据同步
-
权限管理:针对鸿蒙系统的权限模型,优化应用的权限请求流程
- 类型安全:全面使用 TypeScript 类型定义,提高代码质量
- 跨平台兼容性:通过 Base64 图标和平台无关组件,确保在鸿蒙等系统上的良好运行
- 响应式设计:使用 Dimensions API 实现适配不同屏幕尺寸
- 状态管理:合理使用 React Hooks 进行状态管理
- 用户体验:通过 Modal 和 Alert 提供良好的用户反馈
该教师授课打卡系统展示了如何使用 React Native 开发跨平台应用,特别是在鸿蒙系统上的适配实践。通过合理的技术选型和架构设计,实现了一个功能完整、用户友好的授课打卡应用。
真实演示案例代码:
// 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 Teacher = {
id: string;
name: string;
department: string;
position: string;
};
// 教室类型
type Classroom = {
id: string;
name: string;
location: string;
capacity: number;
};
// 授课打卡记录类型
type TeachingAttendanceRecord = {
id: string;
teacherId: string;
classroomId: string;
date: string;
startTime: string;
endTime: string | null;
};
// 教师授课打卡应用组件
const TeachingAttendanceApp: React.FC = () => {
const [teachers] = useState<Teacher[]>([
{
id: '1',
name: '张老师',
department: '计算机学院',
position: '副教授'
},
{
id: '2',
name: '李老师',
department: '数学学院',
position: '讲师'
}
]);
const [classrooms] = useState<Classroom[]>([
{
id: '1',
name: 'A101',
location: '教学楼A栋',
capacity: 50
},
{
id: '2',
name: 'B202',
location: '教学楼B栋',
capacity: 30
}
]);
const [teachingAttendanceRecords, setTeachingAttendanceRecords] = useState<TeachingAttendanceRecord[]>([
{
id: '1',
teacherId: '1',
classroomId: '1',
date: '2023-12-01',
startTime: '09:00',
endTime: '11:00'
}
]);
const [selectedTeacher, setSelectedTeacher] = useState<string | null>(null);
const [selectedClassroom, setSelectedClassroom] = useState<string | null>(null);
const [newAttendanceRecord, setNewAttendanceRecord] = useState({
date: '',
startTime: ''
});
const [isModalVisible, setIsModalVisible] = useState(false);
const [modalContent, setModalContent] = useState('');
// 自动记录授课打卡
useEffect(() => {
const interval = setInterval(() => {
const randomTeacher = teachers[Math.floor(Math.random() * teachers.length)];
const randomClassroom = classrooms[Math.floor(Math.random() * classrooms.length)];
const newRecord: TeachingAttendanceRecord = {
id: (teachingAttendanceRecords.length + 1).toString(),
teacherId: randomTeacher.id,
classroomId: randomClassroom.id,
date: new Date().toISOString().split('T')[0],
startTime: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
endTime: null
};
setTeachingAttendanceRecords([...teachingAttendanceRecords, newRecord]);
}, 60000);
return () => clearInterval(interval);
}, [teachers, classrooms, teachingAttendanceRecords]);
const handleSelectTeacher = (teacherId: string) => {
setSelectedTeacher(teacherId);
Alert.alert('选择教师', '您已选择该教师进行授课打卡');
};
const handleSelectClassroom = (classroomId: string) => {
setSelectedClassroom(classroomId);
Alert.alert('选择教室', '您已选择该教室进行授课打卡');
};
const handleStartClass = () => {
if (newAttendanceRecord.date && newAttendanceRecord.startTime && selectedTeacher && selectedClassroom) {
const newRecord: TeachingAttendanceRecord = {
id: (teachingAttendanceRecords.length + 1).toString(),
teacherId: selectedTeacher,
classroomId: selectedClassroom,
date: newAttendanceRecord.date,
startTime: newAttendanceRecord.startTime,
endTime: null
};
setTeachingAttendanceRecords([...teachingAttendanceRecords, newRecord]);
setNewAttendanceRecord({ date: '', startTime: '' });
Alert.alert('开始授课', '新的授课打卡记录已添加');
} else {
Alert.alert('提示', '请选择教师和教室并填写完整的授课信息');
}
};
const handleEndClass = (recordId: string) => {
const updatedRecords = teachingAttendanceRecords.map(record =>
record.id === recordId ? { ...record, endTime: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) } : record
);
setTeachingAttendanceRecords(updatedRecords);
Alert.alert('结束授课', '授课打卡记录已更新');
};
const handleViewRecord = (recordId: string) => {
const record = teachingAttendanceRecords.find(r => r.id === recordId);
if (record) {
const teacher = teachers.find(t => t.id === record.teacherId);
const classroom = classrooms.find(c => c.id === record.classroomId);
setModalContent(`教师: ${teacher?.name}\n部门: ${teacher?.department}\n职位: ${teacher?.position}\n教室: ${classroom?.name}\n位置: ${classroom?.location}\n容量: ${classroom?.capacity}人\n日期: ${record.date}\n开始时间: ${record.startTime}\n结束时间: ${record.endTime || '未结束'}`);
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>
{teachers.map(teacher => (
<TouchableOpacity
key={teacher.id}
style={[
styles.card,
selectedTeacher === teacher.id && styles.selectedCard
]}
onPress={() => handleSelectTeacher(teacher.id)}
>
<Text style={styles.icon}>👨🏫</Text>
<View style={styles.cardInfo}>
<Text style={styles.cardTitle}>{teacher.name}</Text>
<Text style={styles.cardDescription}>部门: {teacher.department}</Text>
<Text style={styles.cardDescription}>职位: {teacher.position}</Text>
</View>
</TouchableOpacity>
))}
</View>
{/* 教室列表 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>教室列表</Text>
{classrooms.map(classroom => (
<TouchableOpacity
key={classroom.id}
style={[
styles.card,
selectedClassroom === classroom.id && styles.selectedCard
]}
onPress={() => handleSelectClassroom(classroom.id)}
>
<Text style={styles.icon}>🏫</Text>
<View style={styles.cardInfo}>
<Text style={styles.cardTitle}>{classroom.name}</Text>
<Text style={styles.cardDescription}>位置: {classroom.location}</Text>
<Text style={styles.cardDescription}>容量: {classroom.capacity}人</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={newAttendanceRecord.date}
onChangeText={(text) => setNewAttendanceRecord({ ...newAttendanceRecord, date: text })}
/>
<TextInput
style={styles.input}
placeholder="开始时间 (HH:MM)"
value={newAttendanceRecord.startTime}
onChangeText={(text) => setNewAttendanceRecord({ ...newAttendanceRecord, startTime: text })}
/>
</View>
<TouchableOpacity
style={styles.addButton}
onPress={handleStartClass}
>
<Text style={styles.addText}>开始授课</Text>
</TouchableOpacity>
</View>
{/* 授课打卡记录 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>授课打卡记录</Text>
{teachingAttendanceRecords.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.startTime}</Text>
<Text style={styles.cardDescription}>结束时间: {record.endTime || '未结束'}</Text>
</View>
{!record.endTime && (
<TouchableOpacity
style={styles.endButton}
onPress={() => handleEndClass(record.id)}
>
<Text style={styles.endText}>结束授课</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',
},
endButton: {
backgroundColor: '#10b981',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
},
endText: {
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 TeachingAttendanceApp;

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

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

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

本文介绍了一款基于React Native和鸿蒙系统的教师授课打卡应用,通过跨平台技术实现高校教学管理的数字化。应用采用React Native通用API构建核心功能,适配鸿蒙ArkUI原生组件,保留原生交互体验。通过TypeScript构建强类型数据模型,确保跨端数据一致性。利用React Hooks管理状态,实现授课记录的自动生成和手动操作,适配多终端使用场景。该方案充分发挥鸿蒙分布式能力与React Native跨平台优势,为高校教学管理提供高效、精准的数字化解决方案。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐

所有评论(0)