鸿蒙ArkTS企业级活动管理应用:声明式UI与响应式数据架构实践
一、引言:应用架构概览
本文将详细探讨一个基于HarmonyOS ArkTS构建的企业级活动管理应用程序。该应用遵循典型的移动端底部导航模式,集成了活动概览、受邀嘉宾管理、场地评估、预算追踪以及个人工作台五大核心功能模块。项目完整展示了ArkTS在声明式UI开发、状态响应、组件复用及数据驱动视图等方面的强大能力。

从技术层面,本应用采用了清晰的分层设计:最底层是类型定义和数据模型,负责规范全局数据结构;向上是配置映射层,实现业务状态与视觉表现的解耦;再上是业务组件层,每个页面均是独立的@Component结构;顶层是应用的入口组件,通过状态变化驱动页面切换。这种分层架构不仅提高了代码的可维护性,也为未来的功能扩展奠定了坚实基础。
二、核心接口定义:类型安全的基石
interface EventConfigMeta {
displayLabel: string;
iconSymbol: string;
themeColor: string;
bgColor: string;
}
interface ActivityStateMeta {
displayLabel: string;
textColor: string;
iconSymbol: string;
backgroundColor: string;
}
interface PriorityLevelMeta {
name: string;
indicatorColor: string;
emojiIcon: string;
}
interface GuestResponseMeta {
label: string;
color: string;
icon: string;
background: string;
}
interface VenueDetails {
id: number;
name: string;
capacity: number;
dailyRate: number;
facilities: string[];
userRating: number;
locationAddress: string;
}
interface ExpenditureCategory {
id: number;
categoryName: string;
allocatedAmount: number;
spentAmount: number;
displayColor: string;
categoryIcon: string;
}

深入解析
在ArkTS开发中,接口(interface)是确保应用类型安全的重要手段。上述代码定义了六个核心接口,它们各自服务于应用的不同业务领域。
EventConfigMeta、ActivityStateMeta、PriorityLevelMeta 和 GuestResponseMeta 是元数据配置接口,它们的设计统一:包含UI展示所需的所有属性(文本标签、图标符号、主题色、背景色)。这种"配置即数据"的理念将UI的视觉呈现抽象为数据,而非硬编码在组件的build方法中。这样,当需要调整主题或更换图标时,只需修改配置对象,无需修改组件的渲染逻辑。注意PriorityLevelMeta未包含背景色属性,这是因为优先级标签通常以纯文本或小徽章形式出现,无需独立背景。
VenueDetails 和 ExpenditureCategory 则是实体数据接口,描述了业务实体的完整属性集。VenueDetails涵盖了场地的关键信息:ID、名称、容量、每日租金、设施列表、评分及地址。值得注意的是facilities: string[]这一数组类型定义,它允许场地拥有任意数量的设施标签,为后续使用ForEach动态渲染列表提供了类型支持。ExpenditureCategory则侧重财务维度,通过allocatedAmount(预算)和spentAmount(已支出)字段,为预算进度计算和可视化展示提供数据基础,而displayColor和categoryIcon字段则赋予每个预算类别独特的视觉标识。
三、响应式数据模型:@Observed装饰器的巧妙应用
@Observed
export class ActivityRecord {
activityId: number = 0;
eventName: string = '';
eventType: string = '';
scheduledDate: string = '';
scheduledTime: string = '';
eventVenue: string = '';
estimatedBudget: number = 0;
expectedAttendees: number = 0;
currentStatus: string = '待计划';
eventDescription: string = '';
preparationTasks: string[] = [];
urgencyLevel: string = '中';
constructor(
activityId: number, eventName: string, eventType: string, scheduledDate: string,
scheduledTime: string, eventVenue: string, estimatedBudget: number, expectedAttendees: number,
currentStatus: string, eventDescription: string, preparationTasks: string[], urgencyLevel: string
) {
this.activityId = activityId; this.eventName = eventName; this.eventType = eventType; this.scheduledDate = scheduledDate;
this.scheduledTime = scheduledTime; this.eventVenue = eventVenue; this.estimatedBudget = estimatedBudget;
this.expectedAttendees = expectedAttendees; this.currentStatus = currentStatus;
this.eventDescription = eventDescription; this.preparationTasks = preparationTasks; this.urgencyLevel = urgencyLevel;
}
}
@Observed
export class GuestProfile {
guestId: number = 0;
fullName: string = '';
designation: string = '';
affiliation: string = '';
contactInfo: string = '';
rsvpStatus: string = '未回复';
dietaryNeeds: string = '';
additionalNotes: string = '';
constructor(
guestId: number, fullName: string, designation: string, affiliation: string,
contactInfo: string, rsvpStatus: string, dietaryNeeds: string, additionalNotes: string
) {
this.guestId = guestId; this.fullName = fullName; this.designation = designation; this.affiliation = affiliation;
this.contactInfo = contactInfo; this.rsvpStatus = rsvpStatus;
this.dietaryNeeds = dietaryNeeds; this.additionalNotes = additionalNotes;
}
}

深入解析
这是应用中最核心的数据模型定义。ActivityRecord 和 GuestProfile 两个类通过@Observed装饰器标记,这是ArkTS响应式系统的关键组成部分。
@Observed是HarmonyOS ArkTS框架提供的类级状态观察装饰器,其作用是使类的实例成为可观察对象。当类内任何属性发生变化时,所有依赖该实例的UI组件将自动重新渲染。它与@State(组件级状态)、@Prop(单向数据流)和@Link(双向数据流)共同构建了ArkTS完整的状态管理体系。此处选择@Observed而非@State,是因为这些模型类需要在多个组件间共享和传递,且其内部属性的变化需被UI感知。
ActivityRecord类的设计全面覆盖了活动策划的各个维度:基础信息(activityId、eventName、eventType)、时间地点(scheduledDate、scheduledTime、eventVenue)、规模预算(expectedAttendees、estimatedBudget)、状态描述(currentStatus、eventDescription)、执行清单(preparationTasks)及优先级(urgencyLevel)。每个属性都设置了合理的默认值,例如currentStatus默认为'待计划',urgencyLevel默认为'中',这种防御性编程确保了即使构造时遗漏某些参数,对象也能保持有效状态。
构造函数采用了紧凑赋值风格,通过分号将多行赋值合并为一行,在属性较多时能有效减少代码行数。值得注意的是,preparationTasks: string[]的定义允许每个活动拥有可变数量的待办事项,这种数组结构非常适合后续使用ForEach进行动态列表渲染。
GuestProfile类则专注于嘉宾管理的特殊需求。除了姓名、角色、所属机构、联系方式等基本信息外,还包含rsvpStatus(回复状态)、dietaryNeeds(饮食要求)和additionalNotes(备注)等特色字段。其中dietaryNeeds的设计充分考虑了活动餐饮安排的实际业务场景,而additionalNotes字段为策划师记录个性化信息提供了灵活空间。
四、配置映射层:UI与业务状态的解耦艺术
const ACTIVITY_TYPE_MAP: Record = {
'婚庆': { displayLabel: '婚庆', iconSymbol: '💖', themeColor: '#FF5722', bgColor: '#FFF3E0' },
'论坛': { displayLabel: '论坛', iconSymbol: '📊', themeColor: '#BF360C', bgColor: '#FFE0B2' },
'聚会': { displayLabel: '聚会', iconSymbol: '🥳', themeColor: '#FF7043', bgColor: '#FFF3E0' },
'拓展': { displayLabel: '拓展', iconSymbol: '💪', themeColor: '#E64A19', bgColor: '#FFE0B2' }
};
const ACTIVITY_STATUS_MAP: Record = {
'待计划': { displayLabel: '待计划', textColor: '#FF5722', iconSymbol: '🗒️', backgroundColor: '#FFF3E0' },
'进行中': { displayLabel: '进行中', textColor: '#FF7043', iconSymbol: '➡️', backgroundColor: '#FFE0B2' },
'已完成': { displayLabel: '已完成', textColor: '#2E7D32', iconSymbol: '✅', backgroundColor: '#E8F5E9' },
'已取消': { displayLabel: '已取消', textColor: '#9E9E9E', iconSymbol: '🚫', backgroundColor: '#F5F5F5' }
};
const PRIORITY_LEVELS_MAP: Record = {
'高': { name: '高', indicatorColor: '#FF5722', emojiIcon: '🔥' },
'中': { name: '中', indicatorColor: '#FF9800', emojiIcon: '⚠️' },
'低': { name: '低', indicatorColor: '#9E9E9E', emojiIcon: '⏳' }
};
const GUEST_RSVP_MAP: Record = {
'已确认': { label: '已确认', color: '#2E7D32', icon: '✔️', background: '#E8F5E9' },
'未回复': { label: '未回复', color: '#FF5722', icon: '❓', background: '#FFF3E0' },
'已谢绝': { label: '已谢绝', color: '#C62828', icon: '✖️', background: '#FFEBEE' }
};
const ACTIVITY_CATEGORIES: string[] = ['婚庆', '论坛', '聚会', '拓展'];
const CATEGORY_FILTERS: string[] = ['所有', '婚庆', '论坛', '聚会', '拓展'];
const URGENCY_OPTIONS: string[] = ['高', '中', '低'];
const DIETARY_REQUIREMENTS: string[] = ['素食', '清真', '无麸质', '坚果过敏', '乳糖不耐'];

深入解析
配置映射层是本项目中设计模式运用最为精妙的部分。四个Record<string, MetaInterface>类型的配置对象,实现了业务状态值与UI表现之间的完全解耦。
Record<string, EventConfigMeta>是TypeScript提供的一个工具类型,表示一个以字符串为键、以指定类型为值的对象。这里的键(如'婚庆'、'论坛')是业务层面的状态值,而值则是该状态对应的完整视觉配置。这种键值映射模式的优势在于:在组件渲染时,开发者无需编写大量if/else或switch/case来判断当前状态应显示何种颜色和图标,只需通过简单的属性访问即可获取所有视觉参数。例如,ACTIVITY_TYPE_MAP[evt.eventType]?.iconSymbol这一表达式即可根据活动类型动态获取对应的图标,如果类型不在配置中,则通过可选链操作符?.安全回退。
通过观察配色方案,设计师采用了橙色暖色系作为整个应用的主色调。ACTIVITY_TYPE_MAP中不同活动类型的颜色(#FF5722、#BF360C、#FF7043、#E64A19)虽略有差异,但均保持在橙红色相范围内,这种细微变化既区分了类型,又保持了整体视觉统一。背景色(bgColor)则统一使用#FFF3E0或#FFE0B2,它们是主色的极低饱和度版本,作为标签背景既醒目又不刺眼。
ACTIVITY_STATUS_MAP的设计特别关注了状态语义与颜色的对应关系:'已完成'使用绿色系#2E7D32传递积极感,'已取消'使用灰色#9E9E9E传递失效感。GUEST_RSVP_MAP则采用了更鲜明的语义化配色:'已确认'对应绿色系,'未回复'对应橙色系,'已谢绝'对应红色系,用户一眼即可识别嘉宾的回复状态。
四个常量数组ACTIVITY_CATEGORIES、CATEGORY_FILTERS、URGENCY_OPTIONS、DIETARY_REQUIREMENTS则提供了可枚举的业务值集合。CATEGORY_FILTERS在ACTIVITY_CATEGORIES基础上增加了'所有'选项,专为筛选器组件服务,这种细微差异体现了对实际交互场景的深入思考——筛选器需包含一个"不过滤"选项,而类型选择器则不需要。
五、模拟数据层:真实业务场景的数据仿真
const sampleEvents: ActivityRecord[] = [
new ActivityRecord(101, '王总千金喜宴', '婚庆', '2026-08-15', '18:00', '奢华大酒店', 85000, 220, '待计划', '为王总女儿精心策划的婚礼,注重细节。', ['场地勘察', '婚庆公司洽谈', '婚纱挑选', '邀请函设计', '菜单确定', '摄影师预约'], '高'),
new ActivityRecord(102, '2026年度科技峰会', '论坛', '2026-09-10', '09:00', '城市会展中心', 160000, 550, '进行中', '公司年度技术盛典,邀行业大咖分享前沿。', ['会场租赁', '讲师邀约', '议程规划', '宣传物料制作', '线上直播部署'], '高'),
new ActivityRecord(103, '新品发布庆功会', '聚会', '2026-08-01', '19:00', '公司多功能厅', 32000, 160, '待计划', 'V3.0产品成功上线后的庆祝活动。', ['场地布置', '餐饮供应', '音响调试', '嘉宾邀请函'], '中'),
// ... 更多模拟活动数据 (共15条)
new ActivityRecord(115, '年末员工答谢晚宴', '聚会', '2026-12-28', '18:00', '喜来登酒店', 90000, 280, '待计划', '公司年度员工答谢晚宴,营造欢乐氛围。', ['场地确认', '节目编排', '抽奖礼品采购', '餐饮确认', '邀请函发放'], '高')
];
const sampleAttendees: GuestProfile[] = [
new GuestProfile(201, '陈明', '主宾', '陈氏家族', '13812345678', '已确认', '无', '王总的亲戚'),
new GuestProfile(202, '李芳', '伴娘', '新娘好友', '13987654321', '已确认', '素食', '需安排清淡素餐'),
new GuestProfile(203, '张华', '证婚人', '合作企业CEO', '13711112222', '已确认', '无', '准备证婚致辞'),
// ... 更多模拟嘉宾数据 (共18条)
new GuestProfile(218, '何刚', '摄影师', '外包工作室', '13488889999', '已确认', '无', '负责全程影像记录')
];
const sampleLocations: VenueDetails[] = [
{ id: 301, name: '奢华大酒店宴会厅', capacity: 320, dailyRate: 18000, facilities: ['无线网络', '高清投影', '专业音响', '餐饮服务', '免费停车'], userRating: 4.9, locationAddress: '市中心繁华区' },
{ id: 302, name: '城市会展中心大厅', capacity: 1200, dailyRate: 28000, facilities: ['无线网络', 'LED显示屏', '同声传译', '餐饮区', '大型停车场'], userRating: 4.7, locationAddress: '高新开发区' },
// ... 更多模拟场地数据 (共6条)
{ id: 306, name: '滨湖体育公园', capacity: 600, dailyRate: 6000, facilities: ['户外音响', '充足停车位', '更衣淋浴'], userRating: 4.1, locationAddress: '城郊湿地旁' }
];
const sampleBudgetCategories: ExpenditureCategory[] = [
{ id: 401, categoryName: '场地租赁费', allocatedAmount: 220000, spentAmount: 90000, displayColor: '#FF5722', categoryIcon: '🏛️' },
{ id: 402, categoryName: '餐饮酒水', allocatedAmount: 160000, spentAmount: 65000, displayColor: '#FF7043', categoryIcon: '🥂' },
// ... 更多模拟预算数据 (共7项)
{ id: 407, categoryName: '礼品与纪念品', allocatedAmount: 25000, spentAmount: 9000, displayColor: '#BF360C', categoryIcon: '🎁' }
];

深入解析
模拟数据层虽然在技术上只是简单的对象实例化,但从工程实践角度看,它体现了数据驱动的开发范式。15条活动数据、18位嘉宾信息、6个候选场地、7项预算分类,这些数量级并非随意设定,而是经过精心设计的:足以覆盖筛选器测试场景,足以填充列表产生滚动效果,又不会因数据量过大而影响性能或增加代码体积。
sampleEvents数组中的每条数据都经过精心构造,覆盖了四种活动类型(婚庆、论坛、聚会、拓展),时间跨度从2026年7月到2027年2月,预算范围从5000元到160000元,参与人数从20人到550人不等。这种边界值覆盖确保了各组件在各种极端情况下都能正确渲染。特别值得注意的是,preparationTasks数组的长度各不相同(3到6项),这为测试进度条计算的鲁棒性提供了条件。
sampleAttendees数据则展示了嘉宾管理的复杂性:不同的角色(主宾、伴娘、证婚人、讲师等)、不同的RSVP状态分布(14人已确认、3人未回复、1人已谢绝)、不同的饮食要求(素食、清真、坚果过敏)。这种多样性确保了嘉宾列表的筛选和标签渲染逻辑能够得到充分验证。
sampleLocations中的dailyRate: 0(公司多功能厅)是一个特殊边界值,它测试了场地卡片中租金显示逻辑的分支——当价格为0时应显示"免费"而非"¥0/天"。facilities数组的长度差异则测试了设施标签的换行布局能力。
sampleBudgetCategories的颜色采用了渐变色阶设计:从#FF5722到#BF360C,同一色相的不同饱和度和明度变化,在视觉上形成了自然的层次感。每个预算类别的allocatedAmount和spentAmount数值经过精确设定,使得计算出的百分比既不会过于整齐(显得假),也不会过于杂乱(难以验证)。
六、辅助函数层:业务逻辑的轻量封装
function getTotalActivityCount(): number { return 15; }
function getInProgressActivityCount(): number { return 14; }
function getCompletedActivityCount(): number { return 1; }
function getTotalGuestProfiles(): number { return 18; }
function getConfirmedGuestCount(): number { return 14; }
function getPendingGuestCount(): number { return 3; }
function getDeclinedGuestCount(): number { return 1; }
function calculateTotalAllocatedBudget(): number { return 570000; }
function calculateTotalSpentBudget(): number { return 238000; }
function calculateRemainingBudget(): number { return 332000; }
function calculateDaysRemaining(dateString: string): number {
const eventDate: Date = new Date(dateString);
const currentDate: Date = new Date('2026-07-23'); // 模拟当前日期
const timeDifference = eventDate.getTime() - currentDate.getTime();
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24));
}
function getTaskCompletionPercentage(tasks: string[]): number {
// 简化处理:假设每项任务的权重相同,返回任务总数作为进度参考
return tasks.length;
}

深入解析
辅助函数层虽然代码量不大,但其设计思路值得深入探讨。前九个函数采用了硬编码返回值的方式,这在真实项目中通常会被替换为对数据数组的聚合计算(如sampleEvents.length或sampleEvents.filter(e => e.currentStatus === '待计划').length)。但在原型开发或演示场景中,这种硬编码方式具有快速、确定、无副作用的优势。
calculateDaysRemaining函数是其中最具业务价值的函数。它接收一个日期字符串,计算该日期与固定基准日期(2026-07-23)之间的天数差。函数内部使用了标准的JavaScript Date API:先将字符串转换为Date对象,然后通过getTime()获取毫秒时间戳,计算差值后除以一天的毫秒数(1000 * 60 * 60 * 24),最后使用Math.ceil向上取整。这种计算方式简单直接,但需注意时区问题——new Date(dateString)会按本地时区解析字符串,若应用需处理跨时区活动,则需更严谨的时区处理逻辑。
getTaskCompletionPercentage函数的设计则体现了一种简化策略。函数名暗示了"进度"概念,但实际返回的是清单的总项数而非已完成项数。在buildActivityCard中,这个返回值被直接用于进度条宽度计算(getTaskCompletionPercentage(act.preparationTasks) / 8 * 100).toFixed(0),这里的8是一个假设的最大清单项数。这种设计虽然不够精确,但在没有完成状态追踪的简化模型中,它提供了一种直观的进度可视化——项数越多,准备越充分。
七、应用架构层:底部导航与入口组件设计
enum AppNavigationView {
ACTIVITIES = 0,
GUESTS = 1,
VENUES = 2,
BUDGET = 3,
PROFILE = 4
}
@Entry
@Component
struct EventPlannerShell {
@State currentView: AppNavigationView = AppNavigationView.ACTIVITIES;
@Builder mainContentDisplay() {
Column() {
if (this.currentView === AppNavigationView.ACTIVITIES) {
ActivityListPanel();
} else if (this.currentView === AppNavigationView.GUESTS) {
GuestManagementPanel();
} else if (this.currentView === AppNavigationView.VENUES) {
VenueComparisonPanel();
} else if (this.currentView === AppNavigationView.BUDGET) {
BudgetOverviewPanel();
} else {
PlannerDashboardPanel();
}
}
.layoutWeight(1);
}
@Builder bottomNavItem(icon: string, label: string, view: AppNavigationView) {
Column() {
Text(icon).fontSize(20).opacity(this.currentView === view ? 1.0 : 0.45);
Text(label).fontSize(9)
.fontColor(this.currentView === view ? '#FF5722' : '#999999')
.fontWeight(this.currentView === view ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 1 });
if (this.currentView === view) {
Column().width(18).height(3)
.backgroundColor('#FF5722').borderRadius(2).margin({ top: 2 });
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 5, bottom: 5 })
.onClick(() => { this.currentView = view; });
}
build() {
Column() {
this.mainContentDisplay();
Row() {
this.bottomNavItem('📅', '活动', AppNavigationView.ACTIVITIES);
this.bottomNavItem('🧑🤝🧑', '嘉宾', AppNavigationView.GUESTS);
this.bottomNavItem('🏢', '场地', AppNavigationView.VENUES);
this.bottomNavItem('💰', '预算', AppNavigationView.BUDGET);
this.bottomNavItem('👤', '我的', AppNavigationView.PROFILE);
}
.width('100%')
.backgroundColor('#FFFFFF')
.padding({ top: 4, bottom: 6 })
.shadow({ radius: 8, color: '#1A000000', offsetY: -2 });
}
.width('100%').height('100%')
.backgroundColor('#FFF3E0');
}
}

深入解析
这是整个应用的入口组件和导航骨架。@Entry装饰器标记了应用的入口点,而@Component则声明这是一个可复用的UI组件结构。
AppNavigationView枚举的定义是一个最佳实践。使用枚举而非裸数字(0、1、2…)来表示Tab状态,极大提升了代码的可读性和可维护性。当开发者看到AppNavigationView.ACTIVITIES时,其语义一目了然;若写成if (this.currentView === 0),则需额外心智负担来记住每个数字对应的页面。枚举成员被显式赋值为数字,确保了它们在条件判断中可正常工作。
@State currentView: AppNavigationView = AppNavigationView.ACTIVITIES声明了组件的内部状态。@State是ArkTS最基础的状态装饰器,它使currentView成为一个响应式变量。当用户点击底部Tab时,this.currentView = view将触发状态变更,进而导致mainContentDisplay()中的条件渲染重新评估,最终实现页面切换。整个过程中,开发者无需手动操作DOM或调用页面跳转API,框架会自动处理UI更新。
mainContentDisplay()方法被标记为@Builder,这是ArkTS提供的局部构建函数装饰器。@Builder允许开发者将一段UI构建逻辑封装为可复用函数,它可在build()方法中像组件一样被调用(通过this.mainContentDisplay())。mainContentDisplay内部使用了if/else if条件渲染,根据当前激活的Tab实例化对应的内容组件。注意这里使用的是直接组件实例化(如ActivityListPanel())而非路由跳转,这意味着所有页面组件都是同一个父组件的后代,它们可以共享同一上下文和状态管理。
bottomNavItem也是一个@Builder方法,它接收三个参数:图标字符、标签文本和对应的Tab枚举值。这个方法的设计充分体现了参数化组件的思想——通过传入不同参数,可生成外观相似但内容各异的五个Tab按钮。每个Tab按钮内部实现了激活状态视觉反馈:图标透明度从0.45变为1.0,文字颜色从#999999变为#FF5722,字重从Normal变为Bold,且底部会出现一个18x3的橙色指示条。这些视觉变化完全由this.currentView === view这个条件表达式驱动,实现了数据驱动UI的声明式编程范式。
build()方法是每个ArkTS组件必须实现的核心方法,它定义了组件的UI结构。整个布局采用了一个垂直的Column容器,上方是内容区域(通过layoutWeight(1)占据剩余空间),下方是底部Tab栏。Tab栏使用了shadow属性添加了一个向上的微弱阴影(offsetY: -2),使其在视觉上与内容区域产生层次分离。整个页面的背景色设为#FFF3E0,这是应用主题色的极浅色调,为所有内容卡片提供了统一的底色衬托。
八、活动列表模块:搜索、筛选与交互式弹窗
@Component
struct ActivityListPanel {
@State searchFilter: string = '';
@State selectedCategory: string = '所有';
@State showAddActivityForm: boolean = false;
@State showEditActivityForm: boolean = false;
@State currentSelectedActivity: ActivityRecord | null = null; // 联合类型状态
// 表单字段
@State activityFormTitle: string = '';
@State activityFormType: string = '聚会';
@State activityFormDate: string = '2026-08-01';
@State activityFormTime: string = '19:00';
@State activityFormVenue: string = '';
@State activityFormBudget: string = '';
@State activityFormAttendees: string = '';
@State activityFormPriority: string = '中';
@State activityFormDescription: string = '';

深入解析
ActivityListPanel是整个应用中状态最为丰富的组件,它管理了活动列表页面的所有交互状态。
组件内部声明了13个@State变量,可分为三类:筛选状态(searchFilter、selectedCategory)、弹窗控制状态(showAddActivityForm、showEditActivityForm)、表单数据状态(activityFormTitle、activityFormType等)。这种细粒度的状态划分确保了每个交互元素都有独立的数据驱动源。
currentSelectedActivity: ActivityRecord | null = null是一个联合类型状态,它既可以持有一个ActivityRecord实例,也可以是null。这种设计用于编辑弹窗场景:当用户点击某个活动卡片时,currentSelectedActivity被赋值为对应的活动对象;当关闭弹窗时,它被重置为null。联合类型的使用提供了类型安全——在访问currentSelectedActivity属性时,TypeScript编译器会提醒开发者处理null的情况。
表单字段全部使用字符串类型(即使是数字概念如activityFormBudget和activityFormAttendees),这是因为TextInput组件的onChange回调返回的是字符串。这种设计与HTML表单的行为一致:输入框的值始终是字符串,需要数值时再进行类型转换。默认值的选择也体现了业务假设:活动默认类型为"聚会",默认日期为"2026-08-01",默认时间为"19:00",默认优先级为"中"。
弹窗遮罩与新增活动弹窗
@Builder buildModalOverlay(onCloseAction: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(onCloseAction);
}
@Builder newActivityFormModal() {
Column() {
this.buildModalOverlay(() => { this.showAddActivityForm = false; });
Column() {
Row() {
Text('✨ 新建活动').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C');
Column().layoutWeight(1);
Text('✖').fontSize(18).fontColor('#999999')
.onClick(() => { this.showAddActivityForm = false; });
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 });
Divider().color('#FFE0B2');
Scroll() {
Column() {
Text('活动标题').fontSize(12).fontColor('#FF5722').margin({ top: 12, left: 20 });
TextInput({ placeholder: '输入活动名称...' })
.placeholderColor('#FFAB91').fontSize(14).width('100%')
.backgroundColor('#FFF3E0').borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((val: string) => { this.activityFormTitle = val; });
// ... 更多表单字段
Text('活动描述').fontSize(12).fontColor('#FF5722').margin({ top: 12, left: 20 });
TextArea({ placeholder: '详细描述活动内容...' })
.placeholderColor('#FFAB91').fontSize(12).width('100%').height(50)
.backgroundColor('#FFF3E0').borderRadius(8)
.margin({ left: 20, right: 20, top: 4, bottom: 16 })
.onChange((val: string) => { this.activityFormDescription = val; });
}
}
.layoutWeight(1);
Row() {
Text('取消').fontSize(14).fontColor('#888888')
.backgroundColor('#F5F5F5').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.showAddActivityForm = false; });
Text('创建').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#FF5722').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => {
// 模拟创建活动逻辑,实际应保存数据
console.log('New activity created:', this.activityFormTitle);
this.showAddActivityForm = false;
});
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 });
}
.width('90%').height('75%').backgroundColor('#FFFFFF').borderRadius(16)
.alignItems(HorizontalAlign.Center)
.position({ x: '5%', y: '12%' });
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999);
}

深入解析
弹窗系统的设计展现了层级叠加与事件委托的经典模式。
buildModalOverlay是一个通用的遮罩层构建器,它接收一个无参无返回值的回调函数onCloseAction。遮罩层本身是一个全屏的Column,背景色设为半透明的黑色rgba(0,0,0,0.5),并通过.onClick(onCloseAction)绑定了点击事件——当用户点击遮罩层时,弹窗关闭。这种点击外部关闭的交互模式是现代移动端UI的标准做法。
newActivityFormModal的整体结构是一个全屏容器包裹着一个居中卡片。外层Column占据整个屏幕(width('100%').height('100%')),zIndex(999)确保它浮在所有内容之上。内层卡片使用了position({ x: '5%', y: '12%' })进行绝对定位,宽度为屏幕的90%,高度为75%,这种比例化的尺寸定义使得弹窗在不同屏幕尺寸上都能保持良好的视觉效果。
弹窗内部的表单布局采用了分组标签 + 输入控件的模式。每个字段前都有一个Text标签,使用#FF5722橙色和12字号,形成统一的视觉引导。TextInput组件设置了placeholderColor('#FFAB91')和backgroundColor('#FFF3E0'),使得占位符文字和输入背景都融入了应用的主题色系。borderRadius(8)为输入框提供了圆角,符合现代UI设计趋势。
活动类型和优先级的选择器使用了标签组(Chip Group)模式。ForEach遍历ACTIVITY_CATEGORIES或URGENCY_OPTIONS数组,为每个选项渲染一个可点击的标签。当前选中的标签使用白字橙色背景,未选中的标签使用橙字浅色背景,这种双色状态切换的视觉语言直观清晰。onClick事件直接修改对应的表单状态(如this.activityFormType = t),由于这些状态都是@State装饰的,UI会即时响应状态变化,重新渲染标签的选中样式。
底部按钮栏采用了取消 + 创建的经典双按钮布局。两个按钮都使用了大圆角(borderRadius(20))和充足的内边距,形成了易于点击的触控区域。"创建"按钮使用了主题橙色背景配白色文字,而"取消"按钮使用了灰色背景配灰色文字,通过颜色的轻重对比引导用户的操作倾向。
编辑活动弹窗
@Builder editActivityFormModal() {
Column() {
this.buildModalOverlay(() => { this.showEditActivityForm = false; });
Column() {
Row() {
Text('✏️ 修改活动').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C');
Column().layoutWeight(1);
Text('✖').fontSize(18).fontColor('#999999')
.onClick(() => { this.showEditActivityForm = false; });
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 });
Divider().color('#FFE0B2');
Scroll() {
Column() {
Text('活动标题').fontSize(12).fontColor('#FF5722').margin({ top: 12, left: 20 });
TextInput({ placeholder: this.currentSelectedActivity?.eventName ?? '' })
.placeholderColor('#FFAB91').fontSize(14).width('100%')
.backgroundColor('#FFF3E0').borderRadius(8)
.margin({ left: 20, right: 20, top: 4 });
// ... 更多字段
Text('筹备任务(' + (this.currentSelectedActivity?.preparationTasks.length ?? 0) + '项)').fontSize(12)
.fontColor('#FF5722').margin({ top: 12, left: 20 });
Column() {
ForEach(this.currentSelectedActivity?.preparationTasks ?? [], (task: string, index: number) => {
Row() {
Text('▫️').fontSize(14); // 模拟复选框
Text((index + 1).toString() + '. ' + task).fontSize(12)
.fontColor('#555555').layoutWeight(1).margin({ left: 6 });
}
.width('100%').padding({ top: 6, bottom: 6 });
});
}
.width('100%').backgroundColor('#FFF3E0').borderRadius(10)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.margin({ left: 20, right: 20, top: 4, bottom: 16 });
}
}
.layoutWeight(1);
Row() {
Text('保存').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#FF5722').borderRadius(20)
.padding({ left: 28, right: 28, top: 10, bottom: 10 })
.onClick(() => {
// 模拟保存修改逻辑
console.log('Activity updated:', this.currentSelectedActivity?.eventName);
this.showEditActivityForm = false;
});
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 });
}
.width('90%').height('70%').backgroundColor('#FFFFFF').borderRadius(16)
.alignItems(HorizontalAlign.Center)
.position({ x: '5%', y: '15%' });
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999);
}

深入解析
编辑弹窗与新增弹窗在结构上高度相似,但存在几个关键差异,体现了对查看/编辑场景的不同考量。
首先是数据回显方式。编辑弹窗的TextInput使用placeholder而非text来回显当前值:placeholder: this.currentSelectedActivity?.eventName ?? ''。这里使用了可选链操作符?.和空值合并运算符??,当currentSelectedActivity为null时,会安全地回退为空字符串。这种设计虽然简单,但在实际项目中可能需要考虑使用text属性来实现真正的双向绑定。
其次是筹备任务的只读展示。编辑弹窗中包含了一个任务列表,使用ForEach遍历currentSelectedActivity?.preparationTasks ?? []来渲染。每个任务项前面有一个▫️符号,模拟了复选框的视觉效果,但当前实现中它只是一个静态文本,不具备真正的勾选功能。这种半交互式展示的设计暗示了未来可能的扩展方向——为任务项添加完成状态追踪。
编辑弹窗的高度设为70%(比新增弹窗的75%略小),垂直偏移为15%(比新增弹窗的12%略低),这些细微调整使得编辑弹窗在视觉上更加居中,因为编辑表单字段略少于新增表单。
活动卡片构建器
@Builder buildActivityCard(act: ActivityRecord) {
Column() {
Row() {
Column() {
Text(ACTIVITY_TYPE_MAP[act.eventType]?.iconSymbol ?? '🗓️').fontSize(20);
}.width(44).height(44)
.backgroundColor(ACTIVITY_TYPE_MAP[act.eventType]?.bgColor ?? '#FFF3E0').borderRadius(10)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center);
Column() {
Text(act.eventName).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#BF360C')
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis });
Row() {
Text(ACTIVITY_TYPE_MAP[act.eventType]?.displayLabel ?? act.eventType)
.fontSize(10).fontColor(ACTIVITY_TYPE_MAP[act.eventType]?.themeColor ?? '#FF5722')
.backgroundColor(ACTIVITY_TYPE_MAP[act.eventType]?.bgColor ?? '#FFF3E0')
.padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6);
Text(PRIORITY_LEVELS_MAP[act.urgencyLevel]?.emojiIcon ?? '').fontSize(10).margin({ left: 6 });
Text('📍 ' + act.eventVenue).fontSize(10).fontColor('#888888').margin({ left: 6 })
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1);
}
.margin({ top: 4 });
Row() {
Text('👥 ' + act.expectedAttendees + '人').fontSize(10).fontColor('#888888');
Text('💰 ¥' + act.estimatedBudget).fontSize(10).fontColor('#FF5722').margin({ left: 8 });
Text('✔️ ' + getTaskCompletionPercentage(act.preparationTasks) + '项').fontSize(10).fontColor('#888888').margin({ left: 8 });
}
.margin({ top: 3 });
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 });
Column() {
Text(calculateDaysRemaining(act.scheduledDate) <= 0 ? '今日' : calculateDaysRemaining(act.scheduledDate) + '天')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FF5722');
Text('倒计时').fontSize(8).fontColor('#888888');
}.alignItems(HorizontalAlign.End);
}
.width('100%').padding(12);
Row() {
Column()
.width((getTaskCompletionPercentage(act.preparationTasks) / 8 * 100).toFixed(0) + '%') // 假设8项为满进度
.height(4).backgroundColor('#FF5722').borderRadius(2);
Column().layoutWeight(1);
}
.width('100%').height(4).backgroundColor('#FFE0B2').borderRadius(2)
.margin({ left: 12, right: 12, bottom: 12 });
}
.width('100%').backgroundColor('#FFFFFF')
.borderRadius(12).margin({ left: 12, right: 12, top: 5 })
.onClick(() => { this.currentSelectedActivity = act; this.showEditActivityForm = true; });
}
深入解析
buildActivityCard是整个应用中最复杂的UI构建器之一,它定义了活动列表中每个卡片的完整渲染逻辑。
卡片采用了水平三段式布局:左侧是类型图标区,中间是信息主体区,右侧是倒计时区。这种布局在移动端列表中非常常见,因为它能在有限的水平空间内高效地组织多层次信息。
左侧的图标区是一个44x44的圆角方块(borderRadius(10)),背景色和图标字符都通过ACTIVITY_TYPE_MAP配置映射动态获取。?? '🗓️'提供了降级方案——当活动类型不在配置中时,显示一个默认的日历图标。方块内部同时使用了alignItems(HorizontalAlign.Center)和justifyContent(FlexAlign.Center),确保图标在水平和垂直方向都完全居中。
中间的信息主体区通过layoutWeight(1)占据所有剩余空间。标题文本使用了maxLines(1)和textOverflow({ overflow: TextOverflow.Ellipsis }),这意味着当标题过长时会被截断并显示省略号,防止布局溢出。下方的元信息行使用了三个小标签:活动类型标签(带背景色)、优先级图标、场地名称。其中场地名称也设置了单行截断,并通过layoutWeight(1)确保它在行内占据剩余空间,推动倒计时区域靠右对齐。
再下方的统计行展示了三个关键指标:参与人数、预算金额和任务项数。每个指标前面都有一个Emoji图标作为视觉锚点,使得纯文本信息更易于扫描。预算金额使用了#FF5722橙色高亮,吸引用户对财务信息的关注。
右侧的倒计时区域是卡片中最具业务特色的设计。calculateDaysRemaining(act.scheduledDate)计算距离活动的天数,如果小于等于0则显示"今日",否则显示"X天"。这种设计让策划师一眼就能识别最紧急的活动。alignItems(HorizontalAlign.End)确保倒计时文本右对齐,与卡片右侧边缘对齐。
卡片底部是一个进度条,使用两个Column嵌套在一个Row中实现。外层Row设定了总进度条的背景(#FFE0B2),内层的第一个Column根据任务项数计算出填充宽度。这里使用了/ 8 * 100的简化算法,假设8项为满进度。toFixed(0)将百分比取整后拼接为字符串作为宽度值,这种动态计算宽度的模式是ArkTS中实现进度条的典型手法。整个卡片通过.onClick绑定了点击事件,点击后会将当前活动赋值给currentSelectedActivity并打开编辑弹窗。
活动列表页面主体
build() {
Stack() {
Column() {
Row() {
Column() {
Text(getTotalActivityCount().toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF5722');
Text('总活动').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
Column() {
Text(getInProgressActivityCount().toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF7043');
Text('进行中').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
Column() {
Text(getCompletedActivityCount().toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#2E7D32');
Text('已完成').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
Column() {
Text('¥' + (calculateTotalAllocatedBudget() / 10000).toFixed(0) + '万').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E64A19');
Text('总预算').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
}
.width('100%').padding({ top: 14, bottom: 14 })
.backgroundColor('#FFFFFF');
Row() {
Text('🔍').fontSize(14).margin({ left: 10 });
TextInput({ placeholder: '搜索活动...' })
.placeholderColor('#FFAB91').fontSize(13).layoutWeight(1)
.backgroundColor('#FFF3E0').borderRadius(8)
.margin({ left: 6, right: 6 })
.onChange((val: string) => { this.searchFilter = val; });
Text('➕').fontSize(20).fontColor('#FFFFFF')
.backgroundColor('#FF5722').width(30).height(30).borderRadius(15)
.textAlign(TextAlign.Center).margin({ right: 10 })
.onClick(() => { this.showAddActivityForm = true; });
}
.width('100%').padding({ top: 8, bottom: 6 });
Scroll() {
Row() {
ForEach(CATEGORY_FILTERS, (cat: string) => {
if (this.selectedCategory === cat) {
Text(cat === '所有' ? '📋 所有' : ((ACTIVITY_TYPE_MAP[cat]?.iconSymbol ?? '') + ' ' + cat))
.fontSize(11).fontColor('#FFFFFF').backgroundColor('#FF5722')
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(14)
.margin({ left: 3, right: 3 });
} else {
Text(cat === '所有' ? '📋 所有' : ((ACTIVITY_TYPE_MAP[cat]?.iconSymbol ?? '') + ' ' + cat))
.fontSize(11).fontColor('#FF5722').backgroundColor('#FFF3E0')
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(14)
.margin({ left: 3, right: 3 })
.onClick(() => { this.selectedCategory = cat; });
}
});
}
.padding({ left: 8, right: 8 });
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(38);
Scroll() {
Column() {
ForEach(sampleEvents, (eventItem: ActivityRecord) => {
if (this.selectedCategory === '所有' || eventItem.eventType === this.selectedCategory) {
if (eventItem.eventName.includes(this.searchFilter) || this.searchFilter === '') {
this.buildActivityCard(eventItem);
}
}
});
}
.padding({ bottom: 20 });
}
.layoutWeight(1).scrollBar(BarState.Off);
}
.width('100%').height('100%');
if (this.showAddActivityForm) { this.newActivityFormModal(); }
if (this.showEditActivityForm) { this.editActivityFormModal(); }
}
.width('100%').height('100%');
}
深入解析
活动列表页面的build方法采用了Stack作为根容器,这是一种层叠布局策略——弹窗和内容区位于同一堆栈的不同层级,弹窗通过zIndex浮于内容之上。
顶部的统计栏(width('100%').backgroundColor('#FFFFFF'))使用了四个等宽的Column,每个都通过layoutWeight(1)平分宽度,并设置了居中对齐。这种四等分布局在移动端仪表盘设计中非常经典,它将最关键的业务指标(总活动数、进行中、已完成、总预算)以最大的视觉权重呈现给用户。预算数值使用了(calculateTotalAllocatedBudget() / 10000).toFixed(0) + '万'的格式化方式,将大数字转化为更易读的"万"单位。
搜索栏的布局设计巧妙:TextInput通过layoutWeight(1)占据搜索图标和添加按钮之间的所有空间,形成了一种两端固定、中间自适应的经典布局。右侧的添加按钮是一个30x30的圆形(borderRadius(15)),橙色背景配白色加号,textAlign(TextAlign.Center)确保加号在圆形中心对齐。
类型筛选器使用了横向滚动的Scroll容器(scrollable(ScrollDirection.Horizontal)),并隐藏了滚动条(scrollBar(BarState.Off))。筛选标签的设计与弹窗中的类型选择器保持一致,都是选中为白字橙底、未选为橙字浅底。这种视觉一致性降低了用户的学习成本。注意"所有"选项使用了📋图标,而其他选项使用各自类型对应的图标,这种特殊处理让"所有"选项在视觉上与其他类型区分开来。
活动列表使用Scroll包裹,内部通过ForEach遍历sampleEvents并结合筛选条件(selectedCategory和searchFilter)动态渲染每个活动卡片。layoutWeight(1)确保列表区域占据统计栏和搜索栏之外的剩余垂直空间。padding({ bottom: 20 })为列表底部留出足够的空白,避免最后一个卡片紧贴着屏幕底部。
最后的条件渲染if (this.showAddActivityForm) { this.newActivityFormModal(); }是控制弹窗显示/隐藏的关键逻辑。当状态为true时,弹窗构建器被调用,渲染全屏弹窗;当状态为false时,该分支不产生任何UI,弹窗从视图中消失。
九、嘉宾列表模块:RSVP状态管理与信息展示
@Component
struct GuestManagementPanel {
@State currentRsvpFilter: string = '所有';
@Builder buildGuestCard(guest: GuestProfile) {
Column() {
Row() {
Column() {
Text(GUEST_RSVP_MAP[guest.rsvpStatus]?.icon ?? '❓').fontSize(16);
Text(GUEST_RSVP_MAP[guest.rsvpStatus]?.label ?? '未回复').fontSize(8)
.fontColor(GUEST_RSVP_MAP[guest.rsvpStatus]?.color ?? '#FF5722');
}.width(44).height(44)
.backgroundColor(GUEST_RSVP_MAP[guest.rsvpStatus]?.background ?? '#FFF3E0').borderRadius(10)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center);
Column() {
Text(guest.fullName).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#BF360C');
Row() {
Text(guest.designation).fontSize(10).fontColor('#FF5722')
.backgroundColor('#FFF3E0').padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6);
Text('🏢 ' + guest.affiliation).fontSize(10).fontColor('#888888').margin({ left: 6 });
}
.margin({ top: 3 });
Row() {
Text('📞 ' + guest.contactInfo).fontSize(10).fontColor('#888888');
if (guest.dietaryNeeds !== '无' && guest.dietaryNeeds !== '') {
Text('🍽️ ' + guest.dietaryNeeds).fontSize(10).fontColor('#FF7043')
.backgroundColor('#FFE0B2').padding({ left: 4, right: 4, top: 1, bottom: 1 })
.borderRadius(6).margin({ left: 6 });
}
}
.margin({ top: 3 });
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 });
}
.width('100%').padding(12);
if (guest.additionalNotes !== '') {
Text('📝 ' + guest.additionalNotes).fontSize(10).fontColor('#999999')
.padding({ left: 12, right: 12, bottom: 10 });
}
}
.width('100%').backgroundColor('#FFFFFF')
.borderRadius(12).margin({ left: 12, right: 12, top: 5 });
}
深入解析
GuestManagementPanel组件的设计与活动列表有许多共通之处,但在RSVP状态的可视化方面有其独特之处。
每个嘉宾卡片的左侧是一个44x44的状态标识区,其背景色、图标和文字全部通过GUEST_RSVP_MAP配置映射动态获取。这种设计让"已确认"、"未回复"、"已谢绝"三种状态在视觉上产生强烈区分——已确认为绿色、未回复为橙色、已谢绝为红色。状态标签下方还有一个8字号的文字说明,进一步强化了状态信息。
嘉宾信息主体区展示了姓名(14号粗体)、角色标签(橙色小徽章)、所属机构、联系方式和饮食要求。其中饮食要求的条件渲染是一个业务亮点:if (guest.dietaryNeeds !== '无' && guest.dietaryNeeds !== '')确保只有当嘉宾确实有饮食限制时,才会渲染对应的标签。这种条件性UI元素避免了无意义的信息噪音,让界面更加清爽。饮食标签使用了#FF7043橙色系,与角色标签的#FF5722略有区别,形成了信息层级的微妙差异。
备注信息(guest.additionalNotes)采用了卡片内的二级信息展示策略:如果备注不为空,则在主信息行下方追加一行灰色小字。这种设计保持了卡片的主要信息密度适中,同时为额外备注提供了展示空间,不会对所有卡片一视同仁地占用空间。
build() {
Column() {
Row() {
Column() {
Text('🧑🤝🧑 嘉宾管理').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C');
Text('共' + getTotalGuestProfiles() + '位嘉宾 · ' + getConfirmedGuestCount() + '人已确认').fontSize(11).fontColor('#888888').margin({ top: 3 });
}
Column().layoutWeight(1);
Text('➕').fontSize(22).fontColor('#FFFFFF')
.backgroundColor('#FF5722').width(32).height(32).borderRadius(16)
.textAlign(TextAlign.Center);
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 });
Row() {
Column() {
Text(getConfirmedGuestCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#2E7D32');
Text('✔️ 已确认').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
Column() {
Text(getPendingGuestCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FF5722');
Text('❓ 未回复').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
Column() {
Text(getDeclinedGuestCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#C62828');
Text('✖️ 已谢绝').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12 }).padding({ top: 12, bottom: 12 });
Scroll() {
Column() {
ForEach(sampleAttendees, (guest: GuestProfile) => {
if (this.currentRsvpFilter === '所有' || guest.rsvpStatus === this.currentRsvpFilter) {
this.buildGuestCard(guest);
}
});
}
.padding({ bottom: 20 });
}
.layoutWeight(1).scrollBar(BarState.Off);
}
.width('100%').height('100%');
}
深入解析
嘉宾列表页面的顶部是一个标题栏,左侧是主标题和副标题,右侧是一个圆形添加按钮。副标题'共' + getTotalGuestProfiles() + '位嘉宾 · ' + getConfirmedGuestCount() + '人已确认'采用了字符串拼接的方式动态生成统计摘要,让用户在进入页面的瞬间就能了解嘉宾邀约的整体进展。
RSVP状态概览栏使用了三列等分布局,每个状态对应一列。三种状态分别使用了不同的主题色:已确认使用#2E7D32(绿色)、未回复使用#FF5722(橙色)、已谢绝使用#C62828(红色)。这种颜色编码与GUEST_RSVP_MAP中的配置完全一致,确保了视觉语言的统一。每个统计数字使用18号粗体大字,状态标签使用10号灰色小字,形成了清晰的数字-标签视觉层级。
十、场地对比模块:容量可视化与智能推荐
@Component
struct VenueComparisonPanel {
overallMaxCapacity: number = 1200; // 整个应用的场地最大容量参考值
@Builder buildVenueCard(venue: VenueDetails) {
Column() {
Row() {
Column() {
Text('🏢').fontSize(20);
}.width(44).height(44)
.backgroundColor('#FFF3E0').borderRadius(10)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center);
Column() {
Text(venue.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#BF360C');
Row() {
Text('📍 ' + venue.locationAddress).fontSize(10).fontColor('#888888');
Text('⭐ ' + venue.userRating.toString()).fontSize(10).fontColor('#FF5722').margin({ left: 8 });
}
.margin({ top: 3 });
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 });
Column() {
Text(venue.dailyRate === 0 ? '免费' : '¥' + venue.dailyRate + '/天')
.fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FF5722');
Text('日租金').fontSize(8).fontColor('#888888');
}.alignItems(HorizontalAlign.End);
}
.width('100%').padding(12);
Row() {
Text('容量').fontSize(10).fontColor('#888888').width(40);
Column()
.width((venue.capacity / this.overallMaxCapacity * 100).toFixed(0) + '%')
.height(6).backgroundColor('#FF5722').borderRadius(3);
Column().layoutWeight(1);
Text(venue.capacity + '人').fontSize(10).fontColor('#FF5722').fontWeight(FontWeight.Bold).margin({ left: 6 });
}
.width('100%').padding({ left: 12, right: 12, top: 6 });
Row() {
ForEach(venue.facilities, (facility: string) => {
Text('✓ ' + facility).fontSize(9).fontColor('#2E7D32')
.backgroundColor('#E8F5E9').padding({ left: 5, right: 5, top: 2, bottom: 2 })
.borderRadius(6).margin({ left: 2, right: 2 });
});
}
.width('100%').padding({ left: 12, right: 12, top: 8, bottom: 12 });
}
.width('100%').backgroundColor('#FFFFFF')
.borderRadius(12).margin({ left: 12, right: 12, top: 5 });
}
深入解析
VenueComparisonPanel组件专注于场地信息的展示与对比,其设计重点在于数据的可视化呈现。
overallMaxCapacity: number = 1200是一个组件级别的常量变量(未被@State装饰),它定义了容量进度条的最大参考值。所有场地的容量都相对于1200人进行比例计算,这使得进度条具有统一的比较基准。值得注意的是,这个值等于sampleLocations中最大场地(城市会展中心)的容量,确保了最大场地的进度条能填满整个轨道。
场地卡片的信息密度较高,包含了名称、位置、评分、日租金、容量和设施清单。日租金的显示使用了条件表达式venue.dailyRate === 0 ? '免费' : '¥' + venue.dailyRate + '/天',当场地免费(如公司多功能厅)时显示更友好的"免费"文字,而非生硬的"¥0/天"。这种边界值特殊处理体现了对用户体验的细致考量。
容量可视化是该组件的核心亮点。进度条由一个固定宽度的"容量"标签(40宽度)、动态宽度的填充条、自适应的剩余空间和容量数值组成。填充宽度的计算公式(venue.capacity / this.overallMaxCapacity * 100).toFixed(0)将容量转化为百分比后取整,拼接为CSS宽度字符串。进度条高度为6,使用了3的圆角半径,使其呈现为圆角胶囊形状。
设施清单使用了标签云的渲染方式。ForEach(venue.facilities, ...)遍历每个场地的设施数组,为每个设施渲染一个绿色小标签。标签前缀有一个✓符号,暗示"具备此项设施",这种细微的语义符号增强了信息的可理解性。设施标签使用了#2E7D32深绿色和#E8F5E9浅绿背景,与活动卡片的主色调形成对比,让设施信息在视觉上自成一体。
build() {
Column() {
Row() {
Column() {
Text('🏢 场地比较').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C');
Text('共' + sampleLocations.length.toString() + '个备选场地').fontSize(11).fontColor('#888888').margin({ top: 3 });
}
Column().layoutWeight(1);
Text('⚖️').fontSize(20).fontColor('#FF5722');
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 });
Scroll() {
Column() {
ForEach(sampleLocations, (venue: VenueDetails) => {
this.buildVenueCard(venue);
});
Column() {
Text('🏆 推荐场地').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#BF360C')
.width('100%').padding({ left: 16, top: 16, bottom: 8 });
Column() {
Row() {
Text('🥇').fontSize(16);
Column() {
Text('奢华大酒店宴会厅').fontSize(12).fontColor('#BF360C').fontWeight(FontWeight.Medium);
Text('评分4.9 · 适合婚庆/高端活动').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 });
Text('精选').fontSize(10).fontColor('#FFFFFF')
.backgroundColor('#FF5722').padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6);
}
.width('100%').padding({ top: 8, bottom: 8 });
Divider().color('#FFE0B2');
Row() {
Text('🥈').fontSize(16);
Column() {
Text('商务会议中心').fontSize(12).fontColor('#BF360C').fontWeight(FontWeight.Medium);
Text('评分4.7 · 性价比优 · 适合大型论坛').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 });
Text('优选').fontSize(10).fontColor('#FFFFFF')
.backgroundColor('#FF7043').padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6);
}
.width('100%').padding({ top: 8, bottom: 8 });
Divider().color('#FFE0B2');
Row() {
Text('🥉').fontSize(16);
Column() {
Text('公司多功能厅').fontSize(12).fontColor('#BF360C').fontWeight(FontWeight.Medium);
Text('免费 · 适合内部团队聚会').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 });
Text('经济').fontSize(10).fontColor('#FFFFFF')
.backgroundColor('#2E7D32').padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6);
}
.width('100%').padding({ top: 8, bottom: 8 });
}
.padding({ left: 16, right: 16, bottom: 12 });
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 8, bottom: 20 });
}
.padding({ bottom: 20 });
}
.layoutWeight(1).scrollBar(BarState.Off);
}
.width('100%').height('100%');
}
深入解析
场地页面的底部包含了一个智能推荐区域,这是整个模块的增值功能。
推荐卡片标题使用了🏆奖杯图标,暗示这是系统筛选出的优质选项。三个推荐项分别用🥇、🥈、🥉金银铜奖牌图标标注了推荐等级。每个推荐项包含场地名称、推荐理由和状态标签。第一名"奢华大酒店宴会厅"的推荐理由是"评分4.9 · 适合婚庆/高端活动",标签为橙色"精选";第二名"商务会议中心"的标签是浅橙色"优选";第三名"公司多功能厅"的标签则是绿色"经济"。这种分级标签设计不仅展示了推荐排序,还通过不同颜色的标签传递了推荐角度的差异——前两者基于品质推荐,后者基于性价比推荐。
推荐卡片的视觉设计与普通场地卡片保持一致(白色背景、12圆角、左右边距),这种视觉一致性让推荐内容自然地融入列表流中,不会显得突兀。
十一、预算管理模块:财务数据可视化呈现
@Component
struct BudgetOverviewPanel {
@Builder buildBudgetCategoryCard(budget: ExpenditureCategory) {
Column() {
Row() {
Column() {
Text(budget.categoryIcon).fontSize(18);
}.width(36).height(36)
.backgroundColor('#FFF3E0').borderRadius(8)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center);
Column() {
Text(budget.categoryName).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#BF360C');
Text('已支出 ¥' + budget.spentAmount + ' / ¥' + budget.allocatedAmount).fontSize(10).fontColor('#888888').margin({ top: 2 });
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 });
Column() {
Text((budget.spentAmount / budget.allocatedAmount * 100).toFixed(0) + '%').fontSize(14)
.fontWeight(FontWeight.Bold).fontColor(budget.displayColor);
}.alignItems(HorizontalAlign.End);
}
.width('100%').padding(12);
Row() {
Column()
.width((budget.spentAmount / budget.allocatedAmount * 100).toFixed(0) + '%')
.height(6).backgroundColor(budget.displayColor).borderRadius(3);
Column().layoutWeight(1);
}
.width('100%').height(6).backgroundColor('#FFE0B2').borderRadius(3)
.margin({ left: 12, right: 12, bottom: 12 });
}
.width('100%').backgroundColor('#FFFFFF')
.borderRadius(12).margin({ left: 12, right: 12, top: 5 });
}
深入解析
BudgetOverviewPanel组件的设计目标是让财务数据一目了然。
预算卡片采用了左-中-右三段式布局:左侧是类别图标(36x36的圆角方块),中间是类别名称和已支出/预算金额,右侧是百分比数字。图标的大小(36)略小于活动卡片和嘉宾卡片的图标(44),这种尺寸递减的设计与信息的重要性层级相匹配——预算项的视觉权重略低于活动和嘉宾。
百分比的计算(budget.spentAmount / budget.allocatedAmount * 100).toFixed(0)是一个标准的预算使用率公式。toFixed(0)取整后拼接百分号,使得数字简洁易读。百分比的颜色不是固定的,而是使用了budget.displayColor——即每个预算类别在sampleBudgetCategories中定义的专属颜色。这让不同类别的预算进度具有独立的视觉标识,用户可以通过颜色快速区分类别。
进度条的设计与活动卡片的任务进度条类似,但更为精确。它使用每个预算类别的专属颜色作为填充色,背景色统一使用#FFE0B2。当某个类别的支出接近或超过预算时,进度条会接近填满,产生强烈的视觉警示效果。
build() {
Column() {
Text('💰 预算概览').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C')
.width('100%').padding({ left: 16, top: 14, bottom: 8 });
Scroll() {
Column() {
Column() {
Row() {
Column() {
Text('总预算').fontSize(11).fontColor('#888888');
Text('¥' + calculateTotalAllocatedBudget().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#BF360C');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
Column() {
Text('已支出').fontSize(11).fontColor('#888888');
Text('¥' + calculateTotalSpentBudget().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#FF5722');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
Column() {
Text('剩余').fontSize(11).fontColor('#888888');
Text('¥' + calculateRemainingBudget().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#2E7D32');
}.layoutWeight(1).alignItems(HorizontalAlign.Center);
}
.padding({ top: 16, bottom: 16 });
Row() {
Column()
.width((calculateTotalSpentBudget() / calculateTotalAllocatedBudget() * 100).toFixed(0) + '%')
.height(8).backgroundColor('#FF5722').borderRadius(4);
Column().layoutWeight(1);
}
.width('90%').height(8).backgroundColor('#FFE0B2').borderRadius(4)
.margin({ bottom: 12 });
Text('已使用 ' + (calculateTotalSpentBudget() / calculateTotalAllocatedBudget() * 100).toFixed(0) + '%').fontSize(11)
.fontColor('#FF5722').margin({ bottom: 12 });
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 6 })
.alignItems(HorizontalAlign.Center);
Text('📊 详细支出分类').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#BF360C')
.width('100%').padding({ left: 16, top: 16, bottom: 4 });
ForEach(sampleBudgetCategories, (budget: ExpenditureCategory) => {
this.buildBudgetCategoryCard(budget);
});
Column() {
Text('💡 预算洞察').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#BF360C')
.width('100%').padding({ left: 16, top: 12, bottom: 8 });
Column() {
Row() {
Text('📈').fontSize(14);
Text('场地租赁费支出最高,占总预算37%').fontSize(11).fontColor('#555555')
.layoutWeight(1).margin({ left: 8 });
}
.width('100%').padding({ top: 6, bottom: 6 });
Row() {
Text('❗').fontSize(14);
Text('餐饮酒水已使用40%,请注意控制').fontSize(11).fontColor('#FF5722')
.layoutWeight(1).margin({ left: 8 });
}
.width('100%').padding({ top: 6, bottom: 6 });
Row() {
Text('✔️').fontSize(14);
Text('礼品纪念预算充足,目前仅用36%').fontSize(11).fontColor('#2E7D32')
.layoutWeight(1).margin({ left: 8 });
}
.width('100%').padding({ top: 6, bottom: 6 });
}
.padding({ left: 16, right: 16, bottom: 12 });
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 8, bottom: 20 });
}
.padding({ bottom: 20 });
}
.layoutWeight(1).scrollBar(BarState.Off);
}
.width('100%').height('100%');
}
}
深入解析
预算管理页面的顶部是一个财务总览仪表盘,它是整个页面中视觉权重最大的区域。
三个核心财务指标(总预算、已支出、剩余)采用了三列等分 + 超大数字的设计。24号粗体大字确保用户从远处就能看清关键数字。三个数字使用了不同的颜色来传递语义:总预算使用#BF360C(深褐色,代表稳重),已支出使用#FF5722(橙色,代表警示),剩余使用#2E7D32(绿色,代表安全)。这种语义化配色让用户在扫视时就能建立对财务状况的直觉判断。
总进度条高度为8(比分类进度条的6更粗),圆角半径为4(正好是高的一半,形成完美的胶囊形状),宽度限制为90%,两侧留出边距使其在视觉上更加聚焦。进度条下方的百分比文字进一步强化了支出比例信息。
预算洞察区域是财务管理功能的智能化体现。三条提示分别使用了不同的图标和颜色来区分信息级别:第一条是中性信息(📈 + #555555),第二条是警告信息(❗ + #FF5722),第三条是积极信息(✔️ + #2E7D32)。每条提示都是一个Row,左侧是14号图标,右侧是11号说明文字,layoutWeight(1)确保文字占据剩余空间。这种分级信息提示设计帮助策划师快速识别需要关注的财务问题。
十二、策划师个人中心模块:仪表盘与快捷入口
@Component
struct PlannerDashboardPanel {
monthlyActivityCounts: number[] = [2, 3, 2, 4, 3, 1]; // 假设过去6个月的活动数量
monthNames: string[] = ['七月', '八月', '九月', '十月', '十一月', '十二月'];
peakMonthActivityCount: number = 4; // 用于柱状图高度归一化的最大值
build() {
Column() {
Column() {
Row() {
Column() {
Text('🧑💻').fontSize(40);
}.width(64).height(64).backgroundColor('#FFF3E0').borderRadius(32)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center);
Column() {
Text('李思思').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C');
Text('高级活动策划 · 创意无限企划').fontSize(11).fontColor('#888888').margin({ top: 3 });
Text('📧 liss@creativeevent.com').fontSize(10).fontColor('#FF5722').margin({ top: 2 });
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 });
}
.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 16 });
}
.width('100%').backgroundColor('#FFFFFF').margin({ left: 12, right: 12, top: 10 });
深入解析
PlannerDashboardPanel组件模拟了一个策划师的个人工作台,其设计重点在于信息聚合和快捷操作。
monthlyActivityCounts、monthNames和peakMonthActivityCount是三个组件级普通变量(无状态装饰器),它们用于渲染月度活动统计柱状图。peakMonthActivityCount: number = 4是柱状图的最大参考值,等于monthlyActivityCounts数组中的最大值,确保最高柱子能填满预设的图表高度。这种数据与最大值的分离设计,使得当monthlyActivityCounts变化时,只需同步更新peakMonthActivityCount即可保持图表的比例正确。
个人信息卡片采用了头像 + 信息的经典布局。头像是一个64x64的圆形(borderRadius(32)),使用40号的🧑💻Emoji作为占位头像。圆形头像下方是一个信息列,包含姓名(18号粗体)、职位和公司(11号灰色)、邮箱(10号橙色)。邮箱使用橙色是为了暗示它是一个可点击的联系入口,虽然当前实现中没有绑定点击事件,但这种颜色编码为后续的功能扩展预留了视觉线索。
Row() {
Column() {
Text('88').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#FF5722');
Text('已策划').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 14, bottom: 14 })
.backgroundColor('#FFFFFF').borderRadius(10).margin({ left: 6, right: 3 });
Column() {
Text('16').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#FF7043');
Text('进行中').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 14, bottom: 14 })
.backgroundColor('#FFFFFF').borderRadius(10).margin({ left: 3, right: 3 });
Column() {
Text('72').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#2E7D32');
Text('已完成').fontSize(10).fontColor('#888888');
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 14, bottom: 14 })
.backgroundColor('#FFFFFF').borderRadius(10).margin({ left: 3, right: 6 });
}
.width('100%').margin({ top: 8 });
深入解析
统计卡片行使用了三卡片等分布局,与嘉宾页面的RSVP概览栏类似,但这里每个统计项都是一个独立的白色圆角卡片,而非单纯的文本列。
三个统计数字使用了不同的橙色系颜色(#FF5722、#FF7043、#2E7D32),在统一中寻求变化。卡片之间的间距通过margin({ left: 6, right: 3 })、margin({ left: 3, right: 3 })、margin({ left: 3, right: 6 })精细控制,确保三个卡片之间的间隙相等(都是6),而两侧的总边距为12(与页面边距一致)。这种对称间距计算是移动端布局中实现视觉平衡的关键技巧。
Column() {
Text('📈 月度活动量').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#BF360C')
.width('100%').padding({ left: 16, top: 12, bottom: 8 });
Row() {
ForEach([0, 1, 2, 3, 4, 5], (index: number) => {
Column() {
Text(this.monthlyActivityCounts[index].toString())
.fontSize(10).fontColor('#FF5722').margin({ bottom: 3 });
Column()
.width(24)
.height((this.monthlyActivityCounts[index] / this.peakMonthActivityCount * 60).toFixed(0) + 'vp')
.backgroundColor(index === 0 ? '#BF360C' : '#FF7043') // 第一个柱子深色高亮
.borderRadius({ topLeft: 4, topRight: 4 });
Text(this.monthNames[index]).fontSize(9).fontColor('#888888').margin({ top: 3 });
}
.layoutWeight(1).alignItems(HorizontalAlign.Center);
});
}
.padding({ left: 12, right: 12, bottom: 12 });
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 8 });
深入解析
月度活动统计是一个极简的柱状图实现,它完全使用ArkTS的基础组件构建,不依赖任何图表库。
柱状图的数据通过ForEach([0, 1, 2, 3, 4, 5], ...)遍历索引数组来渲染。之所以遍历索引而非直接遍历monthlyActivityCounts,是因为每个柱子需要同时访问monthlyActivityCounts(数值)、monthNames(标签)和索引index(用于高亮当前月)。
每个柱子由三部分组成:顶部的数值标签(10号橙色)、中间的柱体(24宽度、动态高度)、底部的月份标签(9号灰色)。柱体高度通过(this.monthlyActivityCounts[index] / this.peakMonthActivityCount * 60).toFixed(0) + 'vp'计算,其中60是图表的最大高度(单位是vp,即虚拟像素),peakMonthActivityCount作为归一化基准。borderRadius({ topLeft: 4, topRight: 4 })为柱体顶部添加圆角,使其呈现现代的数据可视化风格。
当前月(七月,索引为0)的柱子使用了#BF360C深褐色,其他月份使用#FF7043浅橙色,这种高亮当前月的设计让策划师快速聚焦于眼前的业务高峰。
Column() {
Text('⏰ 即将到来').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#BF360C')
.width('100%').padding({ left: 16, top: 12, bottom: 8 });
Column() {
Row() {
Column() { Text('🥳').fontSize(16); }.width(32);
Column() {
Text('新品发布庆功会').fontSize(12).fontColor('#BF360C').fontWeight(FontWeight.Medium);
Text('八月一日 · 9天后').fontSize(10).fontColor('#FF5722');
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 });
Text('📝').fontSize(14);
}
.width('100%').padding({ top: 8, bottom: 8 });
Divider().color('#FFE0B2');
Row() {
Column() { Text('📊').fontSize(16); }.width(32);
Column() {
Text('季度战略研讨会').fontSize(12).fontColor('#BF360C').fontWeight(FontWeight.Medium);
Text('八月五日 · 13天后').fontSize(10).fontColor('#FF5722');
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 });
Text('📝').fontSize(14);
}
.width('100%').padding({ top: 8, bottom: 8 });
Divider().color('#FFE0B2');
Row() {
Column() { Text('💖').fontSize(16); }.width(32);
Column() {
Text('王总千金喜宴').fontSize(12).fontColor('#BF360C').fontWeight(FontWeight.Medium);
Text('八月十五日 · 23天后').fontSize(10).fontColor('#FF5722');
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 });
Text('📝').fontSize(14);
}
.width('100%').padding({ top: 8, bottom: 8 });
}
.padding({ left: 16, right: 16, bottom: 12 });
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 8 });
深入解析
近期活动列表采用了紧凑的三段式行布局:左侧是32宽度的类型图标列,中间是活动名称和倒计时信息,右侧是状态图标。
倒计时信息'八月一日 · 9天后'将日期和倒计时合并为一行,使用#FF5722橙色强调时间的紧迫感。每两个活动之间使用Divider().color('#FFE0B2')分隔,分隔线颜色比卡片背景略深,但比内容文字浅很多,起到了微妙的区域划分作用而不喧宾夺主。
Column() {
Text('⚙️ 快速入口').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#BF360C')
.width('100%').padding({ left: 16, top: 12, bottom: 8 });
Column() {
Row() { Text('📤').fontSize(18); Text('导出活动方案').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('ᐳ').fontColor('#CCCCCC'); }
.width('100%').padding({ top: 10, bottom: 10, left: 4 });
Divider().color('#FFE0B2');
Row() { Text('🔗').fontSize(18); Text('日历同步设置').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('ᐳ').fontColor('#CCCCCC'); }
.width('100%').padding({ top: 10, bottom: 10, left: 4 });
Divider().color('#FFE0B2');
Row() { Text('🔔').fontSize(18); Text('活动通知管理').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('ᐳ').fontColor('#CCCCCC'); }
.width('100%').padding({ top: 10, bottom: 10, left: 4 });
Divider().color('#FFE0B2');
Row() { Text('📊').fontSize(18); Text('历史数据报告').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('ᐳ').fontColor('#CCCCCC'); }
.width('100%').padding({ top: 10, bottom: 10, left: 4 });
}
.padding({ left: 16, right: 16 });
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 8 });
Column() {
Text('EventPro v2.1 · 活动企划管理 · 2026').fontSize(10).fontColor('#FFAB91')
.margin({ top: 16, bottom: 16 });
}.width('100%').alignItems(HorizontalAlign.Center);
}
.width('100%').height('100%');
}
}
深入解析
快捷操作区域采用了列表菜单的设计模式,每个菜单项都是一个Row,包含图标、标题和右箭头。
菜单项的布局非常紧凑:18号图标作为视觉锚点,13号标题文字通过layoutWeight(1)占据中间空间,ᐳ符号使用#CCCCCC浅灰色,暗示这是一个可点击进入下一级页面的入口。当前实现中这些菜单项没有绑定onClick事件,但完整的视觉结构已经为后续的功能扩展做好了准备。
页面最底部是一个版本信息栏,使用10号浅橙色文字显示应用名称、版本号和年份。这种底部版权信息是移动端应用的标准做法,它为页面提供了视觉上的"收束感",同时传达了产品的品牌信息。
十三、设计模式与架构总结
通过对上述全部代码的逐段解析,我们可以总结出本项目所采用的若干核心设计模式:

- 配置驱动界面 (Configuration-Driven UI):
ACTIVITY_TYPE_MAP、GUEST_RSVP_MAP等配置映射对象实现了业务状态与视觉表现的彻底解耦。当需要调整颜色、图标或新增状态时,只需修改配置对象,无需触碰组件渲染逻辑。 - 响应式数据流 (Reactive Data Flow):
@Observed装饰器配合@State装饰器,构建了从数据模型到UI视图的自动更新链路。开发者只需关注状态的变更,无需手动操作DOM或触发重绘。 - 构建器复用 (Builder Reuse):
@Builder装饰器将重复的UI构建逻辑(如卡片、弹窗、导航项)封装为可复用的函数,减少了代码冗余,提升了可维护性。 - 参数化组件 (Parameterized Components):
bottomNavItem(icon, label, view)、buildActivityCard(act)等方法通过参数接收动态数据,生成结构一致但内容各异的UI元素,这是声明式UI框架中实现列表渲染的核心手法。 - 类型安全保障 (Type Safety Assurance):从接口定义到类属性,从函数参数到状态声明,TypeScript的静态类型系统贯穿始终。可选链(
?.)、空值合并(??)、联合类型(| null)等特性的运用,确保了代码在编译阶段就能捕获潜在的空值错误。 - 视觉一致性原则 (Visual Consistency Principle):整个应用采用统一的橙色系暖色调主题,卡片统一使用白色背景 + 12圆角,列表统一使用12左右边距 + 5上下间距,分隔线统一使用
#FFE0B2。这种严格的视觉规范让应用呈现出专业的产品级质感。
本项目作为HarmonyOS ArkTS的实战范例,完整展示了声明式UI开发的核心范式:通过状态驱动界面、通过配置映射表现、通过组件封装复用、通过类型保障安全。无论是初学者学习ArkTS语法,还是资深开发者研究架构设计,都能从中获得有价值的参考。