【江鸟中原】鸿蒙会议签到小程序
鸿蒙会议签到小程序代码分析报告
一、引言
随着鸿蒙操作系统(HarmonyOS)生态的快速发展,轻量化、原生适配的应用开发逐渐成为企业数字化工具的重要方向。本报告以 “鸿蒙会议签到小程序” 为分析对象,从开发工具、技术框架、项目结构、功能实现、代码逻辑等维度展开详细解析,旨在完整呈现该小程序的技术实现路径与业务价值,为同类鸿蒙应用开发提供参考。
二、开发工具和框架
2.1 开发工具
本项目采用鸿蒙官方指定的集成开发环境 DevEco Studio 5.0+,其核心能力包括:
- 鸿蒙 SDK 支持:内置 API Level 12+(对应 HarmonyOS 3.0 及以上版本),提供 ArkTS、ArkUI 等原生开发工具链;
- 实时预览功能:支持界面代码与设备 / 模拟器界面的实时同步,提升 UI 开发效率;
- 调试工具链:包含日志打印、断点调试、性能分析等功能,支持真机 / 模拟器双环境调试;
- 项目管理能力:自动生成鸿蒙应用标准目录结构,支持模块拆分、依赖管理等工程化需求。
2.2 核心技术框架
本小程序基于鸿蒙原生技术栈开发,核心框架包括:
- ArkTS 语言:TypeScript 的超集,支持静态类型检查、装饰器语法、面向对象编程等特性,是鸿蒙应用开发的首选语言;
- ArkUI 框架(声明式范式):鸿蒙官方 UI 开发框架,采用 “数据驱动 UI” 的设计理念,通过组件化、布局系统、状态管理实现界面快速构建;
- 鸿蒙系统 API:涵盖存储、权限、系统时间、路由等能力,是应用与系统交互的核心接口;
- 关系型数据库(RelationalStore):鸿蒙原生本地数据库方案,基于 SQLite 封装,支持结构化数据的 CRUD 操作。
2.3 技术栈优势
- 原生适配性:ArkUI 与鸿蒙系统深度绑定,相比 H5 等跨端方案,在启动速度、运行流畅度、系统能力调用上更具优势;
- 开发效率:声明式语法 + 组件化思想,大幅减少 UI 代码量,同时支持组件复用;
- 轻量性:依赖本地存储而非服务端,适合线下会议等无网络 / 弱网络场景;
- 多终端兼容:通过 ArkUI 的自适应布局与鸿蒙分布式能力,可快速扩展至平板、智慧屏等终端。
三、项目介绍
3.1 项目结构分析
从截图中的项目目录(左侧文件树)可知,本项目遵循鸿蒙应用的标准工程结构,核心目录如下:
|
目录 / 文件 |
功能说明 |
|
entry |
应用主模块(鸿蒙应用的入口模块),包含代码、资源、配置等核心内容 |
|
entry/src/main/ets |
代码根目录,采用 TypeScript/ArkTS 编写业务逻辑与界面代码 |
|
entry/src/main/ets/entryability |
应用入口能力(EntryAbility),负责应用启动、生命周期管理 |
|
entry/src/main/ets/pages |
页面目录,包含 4 个核心页面(登录页、签到页、签到成功页、统计页)的代码 |
|
entry/src/main/ets/components |
通用组件目录(如签到按钮、统计卡片等可复用组件) |
|
entry/src/main/ets/model |
数据模型目录,定义用户、签到记录等数据结构 |
|
entry/src/main/ets/service |
服务层目录,封装数据存储、业务逻辑等通用能力 |
|
entry/src/main/resources |
资源目录,包含字符串、图片、布局等静态资源 |
|
entry/src/main/module.json5 |
模块配置文件,声明应用权限、页面路由、能力配置等 |
3.2 核心配置文件解析
以module.json5为例,其核心配置项如下:
{
"module": {
"name": "entry",
"type": "entry",
"description": "会议签到小程序主模块",
"mainElement": "EntryAbility",
"deviceTypes": ["phone"], // 优先适配手机端
"pages": [ // 页面路由配置
"pages/LoginPage", // 登录/会议选择页
"pages/SignInPage", // 签到页
"pages/SignSuccessPage", // 签到成功页
"pages/AdminStatsPage" // 管理员统计页
],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "应用入口能力",
"icon": "$media:icon",
"label": "会议签到系统",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background"
}
],
"requestPermissions": [ // 权限申请
{
"name": "ohos.permission.WRITE_USER_STORAGE", // 存储权限
"reason": "需要存储签到记录",
"usedScene": {
"when": "always"
}
}
]
}
}
该文件定义了应用的入口、页面路由、运行设备、权限需求等核心信息,是鸿蒙应用的 “配置中枢”。
3.3 入口能力(EntryAbility)分析
entryability/EntryAbility.ets是应用的启动入口,负责应用生命周期管理,核心代码逻辑如下:
import Ability from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
export default class EntryAbility extends Ability {
onCreate(want, launchParam) {
// 应用创建时执行:初始化数据存储、加载配置
hilog.info(0x0000, 'EntryAbility', 'Application onCreate');
this.initDatabase(); // 初始化本地数据库
}
onWindowStageCreate(windowStage: window.WindowStage) {
// 窗口创建时执行:加载首页
hilog.info(0x0000, 'EntryAbility', 'WindowStage onCreate');
windowStage.loadContent('pages/LoginPage', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'EntryAbility', 'Failed to load content. Cause: %{public}s', JSON.stringify(err));
return;
}
});
}
onDestroy() {
// 应用销毁时执行:释放资源
hilog.info(0x0000, 'EntryAbility', 'Application onDestroy');
}
private initDatabase() {
// 初始化签到记录数据库
// 调用RelationalStore API创建表结构
}
}
其核心作用是:应用启动时初始化资源(如数据库),创建窗口并加载首页,管理应用的生命周期状态。
四、功能介绍(代码级实现)
本小程序包含 “登录 / 会议选择、用户签到、签到成功反馈、管理员统计” 四大功能模块,以下从界面代码、业务逻辑、数据交互三个维度解析每个功能的实现。
4.1 登录 / 会议选择页
4.1.1 界面实现(pages/LoginPage.ets)
该页面负责角色选择、账户输入、会议类型选择,核心代码如下:
@Entry
@Component
struct LoginPage {
// 状态管理:角色(0=职工,1=管理员)、账户、会议类型
@State role: number = 0;
@State account: string = '';
@State meetingType: string = '';
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center, padding: 20 }) {
// 标题
Text('会议签到系统')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 40 });
// 会议类型选择
Text('选择会议类型')
.fontSize(16)
.margin({ bottom: 10, alignSelf: ItemAlign.Start });
TextInput({ placeholder: '输入会议类型' })
.width('100%')
.height(40)
.border({ width: 1, radius: 5, color: '#E5E5E5' })
.margin({ bottom: 20 })
.onChange((value) => this.meetingType = value);
// 角色选择
Flex({ justifyContent: FlexAlign.SpaceAround, width: '100%', margin: { bottom: 20 } }) {
Radio({ value: '0', group: 'role' })
.checked(true)
.onChange((isChecked) => { if (isChecked) this.role = 0; })
.suffixText('职工');
Radio({ value: '1', group: 'role' })
.onChange((isChecked) => { if (isChecked) this.role = 1; })
.suffixText('管理员');
}
// 账户输入
TextInput({ placeholder: '输入账户' })
.width('100%')
.height(40)
.border({ width: 1, radius: 5, color: '#E5E5E5' })
.margin({ bottom: 20 })
.onChange((value) => this.account = value);
// 确定按钮
Button('确定')
.width('100%')
.height(45)
.backgroundColor('#1677FF')
.fontColor('#FFFFFF')
.borderRadius(5)
.onClick(() => {
// 校验输入
if (!this.account) {
promptAction.showToast({ message: '请输入账户' });
return;
}
// 保存角色与账户,跳转至签到页
AppStorage.SetOrCreate('role', this.role);
AppStorage.SetOrCreate('account', this.account);
AppStorage.SetOrCreate('meetingType', this.meetingType);
router.pushUrl({ url: 'pages/SignInPage' });
});
}
.width('100%')
.height('100%');
}
}
代码逻辑说明:
- 采用@Entry+@Component装饰器定义页面组件;
- 通过@State装饰器管理页面状态(角色、账户等),状态变化会自动触发 UI 更新;
- 利用Flex布局实现界面的垂直居中与元素排列;
- 点击 “确定” 按钮时,先校验输入合法性,再通过AppStorage存储用户信息,最后调用router.pushUrl跳转到签到页。
4.1.2 数据存储逻辑
用户信息(角色、账户)通过鸿蒙AppStorage实现跨页面共享:
- AppStorage是鸿蒙提供的应用级状态管理工具,支持键值对存储,数据在应用生命周期内有效;
- 后续页面可通过AppStorage.Get('role')获取当前用户角色,实现权限控制。
4.2 用户签到页
4.2.1 界面实现(pages/SignInPage.ets)
该页面负责用户签到操作,核心代码如下:
@Entry
@Component
struct SignInPage {
// 从AppStorage获取用户信息
@StorageProp('role') role: number = 0;
@StorageProp('account') account: string = '';
@StorageProp('meetingType') meetingType: string = '';
// 当前时间
@State currentTime: string = '';
build() {
Flex({ direction: FlexDirection.Column, padding: 20 }) {
// 头部导航
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center, margin: { bottom: 30 } }) {
Text('返回')
.fontSize(16)
.onClick(() => router.back());
Text('欢迎,' + (this.role === 0 ? '职工' : '管理员'))
.fontSize(16)
.fontWeight(FontWeight.Bold);
}
// 页面标题
Text('用户签到')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 });
Text('请点击下方按钮进行签到')
.fontSize(14)
.fontColor('#999999')
.margin({ bottom: 40 });
// 签到按钮
Button('签到')
.width('100%')
.height(50)
.backgroundColor('#36D399')
.fontColor('#FFFFFF')
.borderRadius(5)
.onClick(() => this.handleSignIn());
// 签到时间
Text('当前时间:' + this.currentTime)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 20 });
}
.width('100%')
.height('100%');
}
// 页面加载时获取当前时间
aboutToAppear() {
this.updateCurrentTime();
// 每秒更新时间
setInterval(() => this.updateCurrentTime(), 1000);
}
// 更新当前时间
private updateCurrentTime() {
const date = new Date();
this.currentTime = date.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
// 处理签到逻辑
private async handleSignIn() {
try {
// 1. 获取当前时间
const signTime = this.currentTime;
// 2. 构造签到记录
const signRecord = {
account: this.account,
role: this.role,
meetingType: this.meetingType,
signTime: signTime,
status: 1 // 1=已签到
};
// 3. 写入本地数据库
await SignInService.addSignRecord(signRecord);
// 4. 跳转至签到成功页,并传递签到信息
router.pushUrl({
url: 'pages/SignSuccessPage',
params: { signRecord: signRecord }
});
} catch (err) {
hilog.error(0x0000, 'SignInPage', 'Sign in failed: %{public}s', JSON.stringify(err));
promptAction.showToast({ message: '签到失败,请重试' });
}
}
}
代码逻辑说明:
- 通过@StorageProp装饰器从AppStorage中读取用户信息(角色、账户),实现跨页面数据共享;
- aboutToAppear生命周期函数在页面加载前执行,用于初始化当前时间,并通过setInterval每秒更新;
- 点击 “签到” 按钮时,调用handleSignIn方法:构造签到记录→调用服务层写入数据库→跳转至签到成功页;
- 签到记录包含 “账户、角色、会议类型、签到时间、状态” 等字段,确保数据的完整性。
4.2.2 服务层逻辑(service/SignInService.ets)
服务层封装了签到记录的数据库操作,核心代码如下:
import relationalStore from '@ohos.data.relationalStore';
import hilog from '@ohos.hilog';
// 数据库名称
const DB_NAME = 'sign_in.db';
// 签到记录表名
const SIGN_TABLE_NAME = 'sign_records';
// 初始化数据库
let rdbStore: relationalStore.RdbStore | null = null;
async function initDatabase() {
if (rdbStore) return;
try {
const context = getContext(this) as common.UIAbilityContext;
rdbStore = await relationalStore.getRdbStore(context, {
name: DB_NAME,
securityLevel: relationalStore.SecurityLevel.S1
});
// 创建签到记录表
await rdbStore.executeSql(`
CREATE TABLE IF NOT EXISTS ${SIGN_TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account TEXT NOT NULL,
role INTEGER NOT NULL,
meetingType TEXT NOT NULL,
signTime TEXT NOT NULL,
status INTEGER NOT NULL
)
`);
hilog.info(0x0000, 'SignInService', 'Database initialized successfully');
} catch (err) {
hilog.error(0x0000, 'SignInService', 'Failed to initialize database: %{public}s', JSON.stringify(err));
throw err;
}
}
// 新增签到记录
export async function addSignRecord(record: any) {
await initDatabase();
if (!rdbStore) throw new Error('Database not initialized');
try {
const valuesBucket = {
account: record.account,
role: record.role,
meetingType: record.meetingType,
signTime: record.signTime,
status: record.status
};
const rowId = await rdbStore.insert(SIGN_TABLE_NAME, valuesBucket);
hilog.info(0x0000, 'SignInService', 'Sign record added, rowId: %{public}d', rowId);
return rowId;
} catch (err) {
hilog.error(0x0000, 'SignInService', 'Failed to add sign record: %{public}s', JSON.stringify(err));
throw err;
}
}
// 查询签到记录(管理员用)
export async function getSignRecords(meetingType: string) {
await initDatabase();
if (!rdbStore) throw new Error('Database not initialized');
try {
const predicates = new relationalStore.RdbPredicates(SIGN_TABLE_NAME);
predicates.equalTo('meetingType', meetingType);
const resultSet = await rdbStore.query(predicates);
// 解析结果集
const records: any[] = [];
while (resultSet.goToNextRow()) {
records.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
account: resultSet.getString(resultSet.getColumnIndex('account')),
role: resultSet.getLong(resultSet.getColumnIndex('role')),
signTime: resultSet.getString(resultSet.getColumnIndex('signTime')),
status: resultSet.getLong(resultSet.getColumnIndex('status'))
});
}
resultSet.close();
return records;
} catch (err) {
hilog.error(0x0000, 'SignInService', 'Failed to get sign records: %{public}s', JSON.stringify(err));
throw err;
}
}
代码逻辑说明:
- initDatabase方法负责创建数据库与签到记录表,表结构包含 “id(主键自增)、account、role、meetingType、signTime、status” 等字段;
- addSignRecord方法将签到记录转换为valuesBucket(键值对),通过rdbStore.insert写入数据库;
- getSignRecords方法通过RdbPredicates构建查询条件(按会议类型筛选),从数据库中查询并返回签到记录,供管理员统计页使用。
4.3 签到成功页
4.3.1 界面实现(pages/SignSuccessPage.ets)
该页面负责展示签到成功的反馈信息,核心代码如下:
@Entry
@Component
struct SignSuccessPage {
// 从路由参数中获取签到记录
@State signRecord: any = {};
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center, padding: 20 }) {
// 图标
Image($r('app.media.success_icon')) // 成功图标(需在resources/media中配置)
.width(80)
.height(80)
.margin({ bottom: 20 });
// 提示文字
Text('签到成功!')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 });
// 签到信息
Text(`签到时间: ${this.signRecord.signTime}`)
.fontSize(16)
.margin({ bottom: 5 });
Text(`会议时间: ${this.signRecord.signTime}`) // 示例中会议时间与签到时间一致,实际可从会议配置中读取
.fontSize(16);
// 返回按钮(普通用户返回首页,管理员返回统计页)
Button(this.signRecord.role === 0 ? '返回首页' : '查看统计')
.width('80%')
.height(45)
.backgroundColor('#1677FF')
.fontColor('#FFFFFF')
.borderRadius(5)
.margin({ top: 40 })
.onClick(() => {
if (this.signRecord.role === 0) {
router.backTo({ url: 'pages/LoginPage' });
} else {
router.pushUrl({ url: 'pages/AdminStatsPage', params: { meetingType: this.signRecord.meetingType } });
}
});
}
.width('100%')
.height('100%');
}
// 页面加载时获取路由参数
aboutToAppear() {
const params = router.getParams();
if (params && params.signRecord) {
this.signRecord = params.signRecord;
}
}
}
代码逻辑说明:
- 通过router.getParams()获取从签到页传递的签到记录参数,并赋值给@State变量;
- 界面展示 “签到时间、会议时间” 等信息,通过角色判断返回按钮的功能(普通用户返回首页,管理员跳转至统计页);
- 依赖resources/media中的图标资源(如success_icon),需在资源目录中提前配置。
4.4 管理员统计页
4.4.1 界面实现(pages/AdminStatsPage.ets)
该页面负责展示签到统计数据与记录,核心代码如下:
@Entry
@Component
struct AdminStatsPage {
// 会议类型
@State meetingType: string = '';
// 签到统计(未签到/应到/已签到)
@State stats: { notSigned: number, total: number, signed: number } = { notSigned: 0, total: 0, signed: 0 };
// 签到记录列表
@State signRecords: any[] = [];
build() {
Flex({ direction: FlexDirection.Column, padding: 20 }) {
// 头部导航
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center, margin: { bottom: 30 } }) {
Text('返回')
.fontSize(16)
.onClick(() => router.back());
Text('管理员界面')
.fontSize(16)
.fontWeight(FontWeight.Bold);
}
// 页面标题
Text('签到统计')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 统计卡片
Flex({ justifyContent: FlexAlign.SpaceAround, width: '100%', margin: { bottom: 30 } }) {
[
{ label: '未签到', value: this.stats.notSigned, color: '#F56C6C' },
{ label: '应到', value: this.stats.total, color: '#409EFF' },
{ label: '已签到', value: this.stats.signed, color: '#36D399' }
].forEach(item => {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) {
Text(item.value.toString())
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(item.color)
.margin({ bottom: 5 });
Text(item.label)
.fontSize(14)
.fontColor('#999999');
}
});
}
// 签到记录标题
Text('签到记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10, alignSelf: ItemAlign.Start });
// 签到记录列表
List({ space: 10 }) {
ForEach(this.signRecords, (item) => {
ListItem() {
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center, padding: 15, border: { width: 1, radius: 5, color: '#E5E5E5' } }) {
Flex({ direction: FlexDirection.Column }) {
Text(item.account)
.fontSize(16)
.fontWeight(FontWeight.Bold);
Text(item.signTime)
.fontSize(12)
.fontColor('#999999');
}
Text(item.status === 1 ? '已签到' : '未签到')
.fontSize(14)
.fontColor(item.status === 1 ? '#36D399' : '#F56C6C');
}
}
});
}
.width('100%')
.flexGrow(1);
}
.width('100%')
.height('100%');
}
// 页面加载时查询数据
aboutToAppear() {
const params = router.getParams();
if (params && params.meetingType) {
this.meetingType = params.meetingType;
this.loadSignData();
}
}
// 加载签到数据(统计+记录)
private async loadSignData() {
try {
// 1. 查询当前会议的所有签到记录
const records = await SignInService.getSignRecords(this.meetingType);
this.signRecords = records;
// 2. 计算统计数据
const total = 10; // 示例中应到人数固定为10,实际可从会议配置中读取
const signed = records.filter(item => item.status === 1).length;
const notSigned = total - signed;
this.stats = { notSigned, total, signed };
} catch (err) {
hilog.error(0x0000, 'AdminStatsPage', 'Failed to load sign data: %{public}s', JSON.stringify(err));
promptAction.showToast({ message: '数据加载失败' });
}
}
}
代码逻辑说明:
- 页面加载时通过router.getParams()获取会议类型,调用loadSignData方法加载数据;
- loadSignData方法从服务层获取签到记录,计算 “未签到 / 应到 / 已签到” 人数(示例中应到人数固定为 10,实际可从会议配置中读取);
- 通过List+ForEach组件渲染签到记录列表,展示账户、签到时间、状态等信息;
- 统计卡片采用循环渲染的方式,减少代码冗余,提升可维护性。
五、项目总结
本鸿蒙会议签到小程序是一款轻量化、实用化的企业工具,其代码实现符合鸿蒙应用开发规范,架构清晰、逻辑完整,充分体现了鸿蒙原生应用的开发特点。尽管存在功能与扩展性上的不足,但对于小型会议签到场景已能满足核心需求,是鸿蒙生态下轻量化应用开发的典型案例。
更多推荐




所有评论(0)