【2026最新】鸿蒙NEXT ArkUI实战:培训班管理系统UI界面开发全攻略
·
鸿蒙UI开发总是踩坑?ArkUI组件用法记不住?本文用15分钟带你彻底搞懂ArkUI核心组件、布局系统、自定义组件和交互动画,附完整培训班管理系统实战代码和踩坑记录,让你的鸿蒙App界面从此丝滑流畅!
一、培训班管理界面设计
1.1 界面设计原则
-
简洁明了:界面布局清晰,信息层次分明
-
操作便捷:常用功能一键直达,减少操作步骤
-
视觉统一:色彩、字体、间距保持一致
-
响应迅速:交互反馈及时,动画流畅自然
1.2 设计规范
|
元素 |
规范 |
说明 |
|---|---|---|
|
主色调 |
#007DFF |
华为蓝,品牌一致性 |
|
辅助色 |
#FF6B6B |
警告/删除 |
|
成功色 |
#52C41A |
成功状态 |
|
字体 |
HarmonyOS Sans |
系统默认字体 |
|
圆角 |
8px |
统一圆角 |
|
间距 |
16px |
基础间距 |
1.3 页面结构
┌─────────────────────────────────────┐
│ 标题栏 (56px) │
├─────────────────────────────────────┤
│ 搜索栏 (48px) │
├─────────────────────────────────────┤
│ │
│ 内容区域 (flex) │
│ │
├─────────────────────────────────────┤
│ 底部导航 (56px) │
└─────────────────────────────────────┘
二、学员列表页面实现
2.1 页面结构
// pages/StudentList.ets
import { Student } from '../model/Student';
import { StudentService } from '../service/StudentService';
@Entry
@Component
struct StudentList {
@State studentList: Student[] = [];
@State isLoading: boolean = true;
@State searchText: string = '';
private studentService: StudentService = new StudentService();
async aboutToAppear() {
await this.loadStudentList();
}
async loadStudentList() {
this.isLoading = true;
this.studentList = await this.studentService.getAllStudents();
this.isLoading = false;
}
build() {
Column() {
// 标题栏
TitleBar({ title: '学员管理', showBack: false })
// 搜索栏
SearchBar({ searchText: $searchText })
// 学员列表
if (this.isLoading) {
LoadingView()
} else if (this.studentList.length === 0) {
EmptyView({ message: '暂无学员数据' })
} else {
List({ space: 12 }) {
ForEach(this.studentList, (student: Student) => {
ListItem() {
StudentCard({ student: student })
}
}, (student: Student) => student.id)
}
.padding(16)
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
2.2 学员卡片组件
// components/student/StudentCard.ets
import { Student } from '../../model/Student';
@Component
export struct StudentCard {
@ObjectLink student: StudentObserved;
clickCallback: (student: Student) => void = () => {};
build() {
Row() {
// 头像
Image(this.student.avatar || $r('app.media.ic_default_avatar'))
.width(48)
.height(48)
.borderRadius(24)
.backgroundColor('#E8E8E8')
// 信息区域
Column() {
// 姓名
Text(this.student.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
// 手机号
Text(this.student.phone)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 4 })
// 标签
Row() {
Text(this.student.gender === 1 ? '男' : '女')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor(this.student.gender === 1 ? '#007DFF' : '#FF6B6B')
.borderRadius(4)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
Text(`${this.student.age}岁`)
.fontSize(12)
.fontColor('#999999')
.margin({ left: 8 })
}
.margin({ top: 8 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(ItemAlign.Start)
// 箭头
Image($r('app.media.ic_arrow_right'))
.width(16)
.height(16)
.fillColor('#CCCCCC')
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.onClick(() => {
this.clickCallback(this.student);
})
}
}
@Observed
class StudentObserved implements Student {
id: string = '';
name: string = '';
phone: string = '';
email: string = '';
gender: number = 0;
birthday: string = '';
address: string = '';
avatar: string = '';
age: number = 0;
createTime: string = '';
updateTime: string = '';
}
2.3 搜索栏组件
// components/common/SearchBar.ets
@Component
export struct SearchBar {
@Link searchText: string;
searchCallback: (text: string) => void = () => {};
build() {
Row() {
// 搜索图标
Image($r('app.media.ic_search'))
.width(20)
.height(20)
.fillColor('#999999')
.margin({ left: 12 })
// 搜索输入框
TextInput({ placeholder: '搜索学员姓名或手机号' })
.layoutWeight(1)
.height(40)
.backgroundColor('transparent')
.fontSize(14)
.placeholderColor('#CCCCCC')
.margin({ left: 8 })
.onChange((value: string) => {
this.searchText = value;
this.searchCallback(value);
})
// 清除按钮
if (this.searchText.length > 0) {
Image($r('app.media.ic_clear'))
.width(16)
.height(16)
.fillColor('#999999')
.margin({ right: 12 })
.onClick(() => {
this.searchText = '';
this.searchCallback('');
})
}
}
.width('100%')
.height(48)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.margin({ left: 16, right: 16, top: 8, bottom: 8 })
}
}
三、课程详情页面实现
3.1 页面结构
// pages/CourseDetail.ets
import { Course } from '../model/Course';
@Entry
@Component
struct CourseDetail {
@State course: Course | null = null;
@State isLoading: boolean = true;
async aboutToAppear() {
// 加载课程详情
await this.loadCourseDetail();
}
async loadCourseDetail() {
this.isLoading = true;
// 从路由参数获取课程ID
const courseId = 'course_001';
this.course = await CourseService.getCourseById(courseId);
this.isLoading = false;
}
build() {
Column() {
// 标题栏
TitleBar({ title: '课程详情', showBack: true })
if (this.isLoading) {
LoadingView()
} else if (this.course) {
Scroll() {
Column() {
// 课程封面
Image(this.course.coverImage)
.width('100%')
.height(200)
.objectFit(ImageFit.Cover)
// 课程信息
Column() {
// 课程名称
Text(this.course.name)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
// 课程描述
Text(this.course.description)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 8 })
.lineHeight(22)
// 课程信息卡片
Row() {
this.InfoItem('授课老师', this.course.teacher)
this.InfoItem('课程时长', `${this.course.duration}课时`)
this.InfoItem('报名人数', `${this.course.currentStudents}/${this.course.maxStudents}`)
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.margin({ top: 16 })
.padding(16)
.backgroundColor('#F8F8F8')
.borderRadius(8)
// 课程大纲
Column() {
Text('课程大纲')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
// 大纲列表
ForEach(this.course.outline, (item: string, index: number) => {
Row() {
Text(`${index + 1}`)
.fontSize(14)
.fontColor('#FFFFFF')
.backgroundColor('#007DFF')
.width(24)
.height(24)
.borderRadius(12)
.textAlign(TextAlign.Center)
Text(item)
.fontSize(14)
.fontColor('#666666')
.margin({ left: 12 })
.layoutWeight(1)
}
.width('100%')
.margin({ top: 12 })
})
}
.width('100%')
.margin({ top: 16 })
}
.padding(16)
}
}
.layoutWeight(1)
// 底部按钮
Row() {
// 价格
Column() {
Text('课程价格')
.fontSize(12)
.fontColor('#999999')
Text(`¥${this.course.price}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6B6B')
}
.alignItems(ItemAlign.Start)
// 报名按钮
Button('立即报名')
.width(120)
.height(44)
.backgroundColor('#007DFF')
.borderRadius(22)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.onClick(() => {
// 跳转报名页面
})
}
.width('100%')
.height(64)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.borderWidth({ top: 1 })
.borderColor('#EEEEEE')
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
InfoItem(label: string, value: string) {
Column() {
Text(value)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(label)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
}
.alignItems(ItemAlign.Center)
}
}
四、报名页面实现
4.1 报名表单
// pages/EnrollmentPage.ets
import { Student } from '../model/Student';
import { Course } from '../model/Course';
import { Enrollment } from '../model/Enrollment';
@Entry
@Component
struct EnrollmentPage {
@State selectedStudent: Student | null = null;
@State selectedCourse: Course | null = null;
@State remark: string = '';
@State isSubmitting: boolean = false;
build() {
Column() {
// 标题栏
TitleBar({ title: '学员报名', showBack: true })
Scroll() {
Column() {
// 选择学员
this.SectionTitle('选择学员')
this.StudentSelector()
// 选择课程
this.SectionTitle('选择课程')
this.CourseSelector()
// 备注信息
this.SectionTitle('备注信息')
TextInput({ placeholder: '请输入备注信息(选填)' })
.width('100%')
.height(100)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.padding(12)
.fontSize(14)
.margin({ top: 8 })
.onChange((value: string) => {
this.remark = value;
})
// 报名信息确认
if (this.selectedStudent && this.selectedCourse) {
this.SectionTitle('报名信息确认')
this.EnrollmentSummary()
}
}
.padding(16)
}
.layoutWeight(1)
// 提交按钮
Button(this.isSubmitting ? '提交中...' : '确认报名')
.width('90%')
.height(48)
.backgroundColor(this.canSubmit() ? '#007DFF' : '#CCCCCC')
.borderRadius(24)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 24 })
.enabled(this.canSubmit() && !this.isSubmitting)
.onClick(() => {
this.submitEnrollment();
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
SectionTitle(title: string) {
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 16, bottom: 8 })
}
@Builder
StudentSelector() {
Row() {
if (this.selectedStudent) {
// 已选择学员
Row() {
Image(this.selectedStudent.avatar || $r('app.media.ic_default_avatar'))
.width(40)
.height(40)
.borderRadius(20)
Column() {
Text(this.selectedStudent.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(this.selectedStudent.phone)
.fontSize(12)
.fontColor('#666666')
}
.margin({ left: 12 })
.alignItems(ItemAlign.Start)
}
.layoutWeight(1)
Text('更换')
.fontSize(14)
.fontColor('#007DFF')
.onClick(() => {
// 打开学员选择弹窗
})
} else {
// 未选择学员
Text('请选择学员')
.fontSize(14)
.fontColor('#CCCCCC')
.layoutWeight(1)
Image($r('app.media.ic_arrow_right'))
.width(16)
.height(16)
.fillColor('#CCCCCC')
}
}
.width('100%')
.height(64)
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.onClick(() => {
// 打开学员选择弹窗
})
}
@Builder
CourseSelector() {
Row() {
if (this.selectedCourse) {
// 已选择课程
Column() {
Text(this.selectedCourse.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(`${this.course.teacher} | ${this.course.duration}课时 | ¥${this.course.price}`)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
}
.alignItems(ItemAlign.Start)
.layoutWeight(1)
Text('更换')
.fontSize(14)
.fontColor('#007DFF')
.onClick(() => {
// 打开课程选择弹窗
})
} else {
// 未选择课程
Text('请选择课程')
.fontSize(14)
.fontColor('#CCCCCC')
.layoutWeight(1)
Image($r('app.media.ic_arrow_right'))
.width(16)
.height(16)
.fillColor('#CCCCCC')
}
}
.width('100%')
.height(64)
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.onClick(() => {
// 打开课程选择弹窗
})
}
@Builder
EnrollmentSummary() {
Column() {
this.SummaryItem('学员姓名', this.selectedStudent?.name || '')
this.SummaryItem('联系电话', this.selectedStudent?.phone || '')
this.SummaryItem('课程名称', this.selectedCourse?.name || '')
this.SummaryItem('授课老师', this.selectedCourse?.teacher || '')
this.SummaryItem('课程费用', `¥${this.selectedCourse?.price || 0}`)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
@Builder
SummaryItem(label: string, value: string) {
Row() {
Text(label)
.fontSize(14)
.fontColor('#666666')
.width(80)
Text(value)
.fontSize(14)
.fontColor('#333333')
.layoutWeight(1)
}
.width('100%')
.height(40)
.borderWidth({ bottom: 1 })
.borderColor('#F0F0F0')
}
canSubmit(): boolean {
return this.selectedStudent !== null && this.selectedCourse !== null;
}
async submitEnrollment() {
if (!this.canSubmit()) return;
this.isSubmitting = true;
try {
const enrollment: Enrollment = {
id: Utils.generateId(),
studentId: this.selectedStudent!.id,
courseId: this.selectedCourse!.id,
enrollTime: Utils.formatDate(new Date()),
status: 0,
remark: this.remark,
createTime: Utils.formatDate(new Date()),
updateTime: Utils.formatDate(new Date())
};
await EnrollmentService.createEnrollment(enrollment);
Utils.showToast('报名成功');
router.back();
} catch (error) {
Utils.showToast('报名失败,请重试');
} finally {
this.isSubmitting = false;
}
}
}
五、响应式布局与多设备适配
5.1 断点系统
// common/BreakpointSystem.ets
export class BreakpointSystem {
private static instance: BreakpointSystem;
private currentBreakpoint: string = 'md';
private constructor() {}
static getInstance(): BreakpointSystem {
if (!BreakpointSystem.instance) {
BreakpointSystem.instance = new BreakpointSystem();
}
return BreakpointSystem.instance;
}
getCurrentBreakpoint(): string {
return this.currentBreakpoint;
}
updateBreakpoint(windowWidth: number): void {
if (windowWidth < 600) {
this.currentBreakpoint = 'sm';
} else if (windowWidth < 840) {
this.currentBreakpoint = 'md';
} else {
this.currentBreakpoint = 'lg';
}
}
}
export const BreakpointConstants = {
SM: 'sm', // 手机
MD: 'md', // 平板
LG: 'lg' // 桌面
};
5.2 响应式布局实现
// components/common/ResponsiveLayout.ets
@Component
export struct ResponsiveLayout {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'md';
build() {
Column() {
if (this.currentBreakpoint === 'sm') {
// 手机布局:单列
this.PhoneLayout()
} else if (this.currentBreakpoint === 'md') {
// 平板布局:双列
this.TabletLayout()
} else {
// 桌面布局:三列
this.DesktopLayout()
}
}
.width('100%')
.height('100%')
}
@Builder
PhoneLayout() {
Column() {
// 单列布局内容
}
.width('100%')
.height('100%')
}
@Builder
TabletLayout() {
Row() {
// 左侧面板
Column() {
// 左侧内容
}
.width('40%')
.height('100%')
// 右侧内容
Column() {
// 右侧内容
}
.layoutWeight(1)
.height('100%')
}
.width('100%')
.height('100%')
}
@Builder
DesktopLayout() {
Row() {
// 左侧面板
Column() {
// 左侧内容
}
.width('25%')
.height('100%')
// 中间内容
Column() {
// 中间内容
}
.layoutWeight(1)
.height('100%')
// 右侧面板
Column() {
// 右侧内容
}
.width('25%')
.height('100%')
}
.width('100%')
.height('100%')
}
}
5.3 媒体查询
// common/MediaQueryHelper.ets
import mediaQuery from '@ohos.mediaquery';
export class MediaQueryHelper {
private static listeners: Map<string, mediaQuery.MediaQueryListener> = new Map();
static registerListener(
condition: string,
callback: (matches: boolean) => void
): void {
const listener = mediaQuery.matchMediaSync(condition);
listener.on('change', (matches: boolean) => {
callback(matches);
});
this.listeners.set(condition, listener);
}
static unregisterListener(condition: string): void {
const listener = this.listeners.get(condition);
if (listener) {
listener.off('change');
this.listeners.delete(condition);
}
}
static isPhone(): boolean {
return !mediaQuery.matchMediaSync('(min-width: 600px)').matches;
}
static isTablet(): boolean {
return mediaQuery.matchMediaSync('(min-width: 600px)').matches &&
!mediaQuery.matchMediaSync('(min-width: 840px)').matches;
}
static isDesktop(): boolean {
return mediaQuery.matchMediaSync('(min-width: 840px)').matches;
}
}
六、交互动画与用户体验优化
6.1 页面转场动画
// common/TransitionAnimation.ets
export class TransitionAnimation {
static slideInFromRight(): TransitionEffect {
return TransitionEffect.asymmetric(
TransitionEffect.move(TransitionEdge.START)
.animation({ duration: 300, curve: Curve.EaseInOut }),
TransitionEffect.move(TransitionEdge.END)
.animation({ duration: 300, curve: Curve.EaseInOut })
);
}
static fadeIn(): TransitionEffect {
return TransitionEffect.opacity(0)
.animation({ duration: 200, curve: Curve.EaseInOut });
}
static scaleIn(): TransitionEffect {
return TransitionEffect.scale({ x: 0.8, y: 0.8 })
.animation({ duration: 200, curve: Curve.EaseInOut });
}
}
6.2 列表项动画
// components/common/AnimatedListItem.ets
@Component
export struct AnimatedListItem {
@State isVisible: boolean = false;
@Prop index: number = 0;
aboutToAppear() {
// 延迟显示,实现依次出现效果
setTimeout(() => {
this.isVisible = true;
}, this.index * 50);
}
build() {
Column() {
// 列表项内容
}
.width('100%')
.opacity(this.isVisible ? 1 : 0)
.translate({ y: this.isVisible ? 0 : 20 })
.animation({
duration: 300,
curve: Curve.EaseInOut,
delay: this.index * 50
})
}
}
6.3 按钮点击反馈
// components/common/ActionButton.ets
@Component
export struct ActionButton {
@Prop label: string = '';
@Prop icon: Resource | null = null;
@State isPressed: boolean = false;
clickCallback: () => void = () => {};
build() {
Column() {
if (this.icon) {
Image(this.icon)
.width(24)
.height(24)
.fillColor(this.isPressed ? '#007DFF' : '#666666')
}
Text(this.label)
.fontSize(12)
.fontColor(this.isPressed ? '#007DFF' : '#666666')
.margin({ top: 4 })
}
.width(64)
.height(64)
.justifyContent(FlexAlign.Center)
.scale({ x: this.isPressed ? 0.95 : 1, y: this.isPressed ? 0.95 : 1 })
.animation({ duration: 100, curve: Curve.EaseInOut })
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.isPressed = true;
} else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
this.isPressed = false;
if (event.type === TouchType.Up) {
this.clickCallback();
}
}
})
}
}
6.4 下拉刷新
// components/common/PullToRefresh.ets
@Component
export struct PullToRefresh {
@Prop isRefreshing: boolean = false;
refreshCallback: () => void = () => {};
build() {
Refresh({ refreshing: $$this.isRefreshing }) {
// 列表内容
}
.onRefreshing(() => {
this.refreshCallback();
})
}
}
6.5 加载状态动画
// components/common/LoadingView.ets
@Component
export struct LoadingView {
@State rotationAngle: number = 0;
aboutToAppear() {
this.startRotation();
}
startRotation() {
setInterval(() => {
this.rotationAngle += 10;
}, 50);
}
build() {
Column() {
// 加载图标
Image($r('app.media.ic_loading'))
.width(40)
.height(40)
.rotate({ angle: this.rotationAngle })
// 加载文字
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 12 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
七、踩坑记录
7.1 组件复用问题
问题:ForEach渲染的列表项状态混乱。
原因:ForEach的keyGenerator函数返回值不唯一。
解决方案:
ForEach(this.list, (item: Item) => {
// 列表项内容
}, (item: Item) => item.id) // 确保id唯一
7.2 动画性能问题
问题:复杂动画导致界面卡顿。
原因:动画属性过多或动画时长过长。
解决方案:
// 使用硬件加速
.animation({
duration: 300,
curve: Curve.EaseInOut,
tempo: 1.0, // 使用默认节奏
iterations: 1
})
7.3 响应式布局问题
问题:不同设备上布局错乱。
原因:使用固定尺寸而非相对尺寸。
解决方案:
// 使用layoutWeight而非固定宽度
.layoutWeight(1)
// 使用百分比而非固定像素
.width('80%')
八、总结与预告
本文要点回顾
-
界面设计:遵循设计规范,保持视觉统一
-
组件开发:封装可复用组件,提高开发效率
-
列表实现:实现学员列表、课程详情、报名页面
-
响应式布局:适配手机、平板、桌面多设备
-
交互动画:提升用户体验,增加界面流畅感
下期预告
下期我们将深入讲解状态管理篇,包括:
-
学员信息状态管理
-
课程数据状态流转
-
报名流程状态控制
-
@State、@Prop、@Link实战应用
互动引导
如果本文对你有帮助,请点赞、收藏、关注!有任何问题欢迎在评论区留言,我会及时回复。
系列文章导航:
-
第1篇:项目架构篇
-
第2篇:UI界面篇(本文)
-
第3篇:状态管理篇
-
第4篇:数据持久化篇
-
第5篇:性能优化篇
更多推荐

所有评论(0)