欢迎加入开源鸿蒙PC社区:
https://harmonypc.csdn.net/

atomgit仓库地址:https://atomgit.com/Math_teacher_fan/todolist
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、项目背景与设计理念

任务管理是每个人日常生活中都会面临的需求。从简单的购物清单到复杂的工作项目规划,任务管理工具无处不在。然而,传统的任务管理应用往往存在几个问题:界面复杂、使用繁琐、缺乏上下文感知能力、无法灵活适应用户的实际需求。

TodoList 任务详情子系统的设计初衷是解决这些痛点。应用采用"可选描述字段"的理念,任务标题是必填的,但描述是可选的。这种设计既保证了任务的清晰表达,又不会因为过于繁琐而增加用户负担。同时,应用具有"上下文感知渲染"能力,能够根据当前时间、任务状态等上下文信息,智能地展示相关信息,帮助用户更高效地管理任务。

从技术角度看,这个项目是Electron与OpenHarmony融合框架的一次实践。通过web_engine模块,开发者可以使用熟悉的Web技术栈开发鸿蒙桌面应用。应用的核心逻辑全部用JavaScript实现,界面采用响应式设计,这种技术选型让代码维护变得简单,也让更多开发者能够参与到项目中来。

二、系统架构设计

2.1 三层架构模型

TodoList应用采用经典的三层架构模型,将视图层、样式层和业务逻辑层分离。这种架构设计的好处是职责清晰、维护方便、扩展灵活。

视图层负责页面的结构布局和组件组织。HTML文件定义了应用的整体框架,包括头部导航、左侧的任务输入和任务列表区域、右侧的任务详情和上下文信息区域。左侧区域包含新建任务表单、任务列表、筛选器和统计信息四个核心模块。右侧区域包含任务详情面板、上下文信息面板和快捷操作面板三个辅助模块。这种左右分栏的布局设计让用户能够同时查看任务列表和任务详情,提高了操作效率。

样式层负责应用的视觉呈现。CSS文件定义了所有组件的样式,包括颜色、字体、间距、动画效果等。应用采用CSS变量系统,将主题色、背景色、边框色等统一定义在根选择器中,方便后续的主题切换和样式调整。紫色系配色方案从浅紫到深紫形成渐变层次,配合圆角边框和阴影效果,营造出专业而现代的视觉感受。

业务逻辑层负责应用的核心功能实现。JavaScript文件包含了任务管理、上下文感知渲染、数据持久化等所有业务逻辑。代码采用模块化的组织方式,每个功能模块都有独立的函数集合,通过状态对象统一管理应用数据。这种设计让代码结构清晰,便于后续的功能扩展和维护。

2.2 数据流设计

应用的数据流采用单向数据流模式。用户操作触发事件处理函数,函数更新状态对象,状态变化触发界面重新渲染。这种数据流设计保证了数据的一致性,避免了状态混乱的问题。

状态对象是整个应用的核心数据容器。它包含三个主要属性:tasks存储任务数组,selectedTaskId存储当前选中的任务ID,currentFilter存储当前的筛选条件。所有数据操作都通过这个状态对象进行,确保数据的集中管理和统一更新。

界面渲染函数负责将状态数据转换为可视化的DOM元素。每个功能模块都有对应的渲染函数,如renderTaskList负责渲染任务列表,renderTaskDetail负责渲染任务详情,updateContextInfo负责更新上下文信息。这些函数读取状态数据,生成HTML字符串,然后插入到对应的容器元素中。当状态数据发生变化时,只需调用对应的渲染函数,界面就会自动更新。

2.3 模块化设计

应用采用模块化设计,将不同功能封装到独立的函数中。主要模块包括任务管理模块、上下文感知模块、数据持久化模块和UI渲染模块。

任务管理模块负责任务的增删改查操作。包括addTask函数添加新任务,deleteTask函数删除任务,toggleTaskComplete函数切换任务完成状态,selectTask函数选择任务。这些函数修改appState.tasks数组,然后调用saveState和renderTaskList更新界面。

上下文感知模块负责根据上下文信息更新界面显示。包括updateContextInfo函数更新上下文信息面板,getDueDateInfo函数计算截止日期相关信息,startClock函数启动实时时钟。这些函数读取appState.tasks数组和当前时间,计算上下文信息,然后更新相应的DOM元素。

数据持久化模块负责应用数据的本地存储和读取。包括saveState函数保存状态到localStorage,loadState函数从localStorage读取状态。这些函数使用JSON序列化和反序列化,实现数据的持久化。

UI渲染模块负责生成HTML内容和更新DOM。包括renderTaskList函数渲染任务列表,renderTaskDetail函数渲染任务详情,updateTaskStats函数更新任务统计信息。这些函数使用模板字符串生成HTML内容,使用innerHTML属性更新DOM。

三、核心功能实现详解

3.1 任务数据结构设计

任务数据是整个应用的核心数据源。应用采用对象数组的形式组织任务数据,每个任务对象包含多个属性字段。

{
    id: Date.now(),
    title: '任务标题',
    description: '任务描述',
    priority: 'high',
    category: 'work',
    dueDate: '2026-06-01',
    completed: false,
    createdAt: new Date().toISOString()
}

这个数据结构设计有几个关键点。首先是id字段,作为任务的唯一标识符,使用Date.now()生成时间戳。其次是title字段,存储任务标题,这是必填字段。再次是description字段,存储任务描述,这是可选字段。priority字段存储优先级,有high、medium、low三个值。category字段存储分类标签,有work、personal、study、shopping、other五个值。dueDate字段存储截止日期,格式为YYYY-MM-DD。completed字段存储完成状态,布尔值。createdAt字段存储创建时间,使用ISO格式字符串。

这种数据结构设计的好处是信息完整、结构清晰、易于扩展。如果需要添加新的任务属性,只需在对象中添加新的字段,然后修改渲染函数即可。比如需要添加任务标签,只需添加tags字段。

3.2 任务添加功能实现

任务添加是应用的核心功能之一,handleAddTask函数实现了完整的添加逻辑。

function handleAddTask() {
    const title = document.getElementById('taskTitle').value.trim();
    const description = document.getElementById('taskDescription').value.trim();
    const priority = document.getElementById('taskPriority').value;
    const dueDate = document.getElementById('taskDueDate').value;
    const category = appState.selectedCategory || 'other';
    
    if (!title) {
        showToast('请输入任务标题!');
        document.getElementById('taskTitle').focus();
        return;
    }
    
    const task = {
        id: Date.now(),
        title: title,
        description: description,
        priority: priority,
        category: category,
        dueDate: dueDate,
        completed: false,
        createdAt: new Date().toISOString()
    };
    
    appState.tasks.unshift(task);
    saveState();
    renderTaskList();
    updateTaskStats();
    updateContextInfo();
    
    document.getElementById('taskTitle').value = '';
    document.getElementById('taskDescription').value = '';
    document.getElementById('taskDueDate').value = '';
    document.getElementById('taskPriority').value = 'medium';
    document.querySelectorAll('#categoryChips .chip').forEach(c => c.classList.remove('selected'));
    appState.selectedCategory = null;
    updateCharCount('taskTitle', 'titleCharCount', 100);
    updateCharCount('taskDescription', 'descCharCount', 500);
    
    showToast('任务添加成功!');
}

这段代码展示了任务添加的完整流程。首先读取表单输入的值,包括标题、描述、优先级、截止日期和分类。标题使用trim方法去除首尾空白,如果为空则显示提示并返回。

然后创建任务对象,使用Date.now()生成唯一ID。unshift方法将新任务添加到数组开头,保证最新添加的任务显示在最前面。

最后调用saveState保存状态,renderTaskList渲染列表,updateTaskStats更新统计,updateContextInfo更新上下文信息。同时清空表单,为下次添加做准备。

3.3 上下文感知渲染实现

上下文感知渲染是应用的核心亮点,updateContextInfo函数实现了这一功能。

function updateContextInfo() {
    const now = new Date();
    const timeStr = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
    const dateStr = now.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' });
    
    document.getElementById('currentTime').textContent = timeStr;
    document.getElementById('todayDate').textContent = dateStr;
    
    const total = appState.tasks.length;
    const completed = appState.tasks.filter(t => t.completed).length;
    const progress = total > 0 ? Math.round((completed / total) * 100) : 0;
    document.getElementById('contextStats').textContent = `${progress}% 完成 (${completed}/${total})`;
    document.getElementById('contextStats').className = progress >= 80 ? 'context-value success' : 'context-value';
    
    const urgentTasks = appState.tasks.filter(t => {
        if (t.completed || !t.dueDate) return false;
        const due = new Date(t.dueDate);
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        const dueDay = new Date(due);
        dueDay.setHours(0, 0, 0, 0);
        const diffDays = Math.ceil((dueDay - today) / (1000 * 60 * 60 * 24));
        return diffDays <= 2;
    });
    
    if (urgentTasks.length > 0) {
        document.getElementById('contextTrend').textContent = `⚠️ ${urgentTasks.length}项紧急任务`;
        document.getElementById('contextTrend').className = 'context-value urgent';
    } else if (progress >= 80) {
        document.getElementById('contextTrend').textContent = '🎉 进度优秀!';
        document.getElementById('contextTrend').className = 'context-value success';
    } else {
        document.getElementById('contextTrend').textContent = '💪 继续加油!';
        document.getElementById('contextTrend').className = 'context-value';
    }
}

这段代码展示了上下文感知渲染的实现逻辑。函数首先获取当前时间,使用LocaleString方法格式化为中文显示。然后更新当前时间和日期的DOM元素。

接下来计算任务统计信息。使用filter方法分别统计总数和已完成数,计算完成百分比。根据百分比值设置contextStats的样式类,完成率达到80%以上时使用success样式。

然后计算紧急任务。筛选条件是:未完成、有截止日期、截止日期在2天以内。计算方法是将截止日期和今天都设置为零点,然后计算天数差。

最后根据紧急任务数量和完成进度更新趋势信息。紧急任务多于0时显示警告提示;完成进度达到80%以上时显示鼓励信息;否则显示加油提示。

3.4 截止日期智能判断实现

截止日期的智能判断是上下文感知的重要体现,getDueDateInfo函数实现了这一功能。

function getDueDateInfo(dueDate) {
    if (!dueDate) return { display: '', isUrgent: false };
    
    const due = new Date(dueDate);
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const dueDay = new Date(due);
    dueDay.setHours(0, 0, 0, 0);
    
    const diffDays = Math.ceil((dueDay - today) / (1000 * 60 * 60 * 24));
    let display = '';
    let isUrgent = false;
    
    if (diffDays === 0) {
        display = '今天';
        isUrgent = true;
    } else if (diffDays === 1) {
        display = '明天';
        isUrgent = true;
    } else if (diffDays === -1) {
        display = '昨天(已过期)';
        isUrgent = true;
    } else if (diffDays < -1) {
        display = `${Math.abs(diffDays)}天前(已过期)`;
        isUrgent = true;
    } else {
        display = `${diffDays}天后`;
        isUrgent = diffDays <= 3;
    }
    
    return { display, isUrgent };
}

这段代码展示了截止日期智能判断的逻辑。首先检查dueDate是否为空,如果为空返回空显示和非紧急状态。

然后计算天数差。将截止日期和今天都设置为零点(清除时间部分),计算两个日期的差值。Math.ceil确保结果为整数天数。

根据diffDays的值判断显示文本和紧急状态。0表示今天,1表示明天,-1表示昨天,小于-1表示多天前,大于1表示多天后。紧急状态的判断逻辑是:今天、明天、昨天、多天前都标记为紧急,大于1但小于等于3天也标记为紧急。

3.5 任务筛选功能实现

任务筛选功能允许用户查看不同状态的任务,renderTaskList函数结合筛选条件实现这一功能。

function renderTaskList() {
    const container = document.getElementById('taskList');
    let filteredTasks = appState.tasks;
    
    if (appState.currentFilter === 'active') {
        filteredTasks = appState.tasks.filter(task => !task.completed);
    } else if (appState.currentFilter === 'completed') {
        filteredTasks = appState.tasks.filter(task => task.completed);
    }
    
    if (filteredTasks.length === 0) {
        container.innerHTML = `
            <div class="empty-state">
                <div class="empty-icon">📭</div>
                <p>${appState.currentFilter === 'active' ? '暂无待办任务' : 
                   appState.currentFilter === 'completed' ? '暂无已完成任务' : '暂无任务,添加一个开始吧!'}</p>
            </div>
        `;
        return;
    }
    
    container.innerHTML = filteredTasks.map(task => {
        const isSelected = task.id === appState.selectedTaskId;
        const categoryIcon = getCategoryIcon(task.category);
        const priorityBadge = getPriorityBadge(task.priority);
        const dueDateInfo = getDueDateInfo(task.dueDate);
        
        return `
            <div class="task-card ${task.completed ? 'completed' : ''} ${isSelected ? 'selected' : ''}" data-id="${task.id}">
                <div class="task-card-header">
                    <div class="task-title">${escapeHtml(task.title)}</div>
                    <span class="task-priority-badge ${task.priority}">${priorityBadge}</span>
                </div>
                ${task.description ? `<div style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 8px;">${escapeHtml(task.description.substring(0, 80))}${task.description.length > 80 ? '...' : ''}</div>` : ''}
                <div class="task-card-meta">
                    <span>${categoryIcon} ${getCategoryLabel(task.category)}</span>
                    ${task.dueDate ? `<span class="${dueDateInfo.isUrgent ? 'task-priority-badge high' : ''}">📅 ${dueDateInfo.display}</span>` : ''}
                </div>
                <div class="task-actions">
                    <button class="task-action-btn" onclick="toggleTaskComplete(${task.id})" title="${task.completed ? '标记未完成' : '标记完成'}">
                        ${task.completed ? '↩️' : '✓'}
                    </button>
                    <button class="task-action-btn" onclick="deleteTask(${task.id})" title="删除">
                        🗑️
                    </button>
                </div>
            </div>
        `;
    }).join('');
    
    container.querySelectorAll('.task-card').forEach(card => {
        card.addEventListener('click', (e) => {
            if (e.target.closest('.task-action-btn')) return;
            selectTask(parseInt(card.dataset.id));
        });
    });
}

这段代码展示了任务筛选和渲染的完整流程。首先根据currentFilter筛选任务数组。all显示全部任务,active显示未完成任务,completed显示已完成任务。

如果筛选结果为空,显示空状态提示,提示内容根据筛选条件动态变化。

如果筛选结果不为空,使用map方法遍历数组,为每个任务生成HTML元素。任务卡片包含完成状态样式、选中状态样式、优先级徽章、描述预览、分类和截止日期信息。

描述预览使用substring方法截取前80个字符,超过80字符的部分用省略号代替。任务卡片的点击事件绑定在父容器上,使用事件委托模式,避免为每个卡片单独绑定事件。

四、界面设计与交互优化

4.1 响应式布局设计

应用采用响应式布局设计,适配不同屏幕尺寸。布局系统基于CSS Grid实现,通过媒体查询调整网格配置。

.main-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 24px;
}

@media (max-width: 1200px) {
    .main-content {
        grid-template-columns: 1fr;
    }
    
    .detail-meta-grid {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 768px) {
    .header-content {
        flex-direction: column;
        gap: 16px;
    }
    
    .row-fields {
        grid-template-columns: 1fr;
    }
    
    .quick-actions {
        grid-template-columns: 1fr;
    }
}

这段CSS代码展示了响应式布局的核心配置。main-content容器使用grid布局,两栏等宽。当屏幕宽度小于1200px时,切换为单栏布局,详情元数据也改为单列。当屏幕宽度小于768px时,进一步调整内部元素的布局,头部内容改为垂直排列,表单字段改为单列,快捷操作改为单列。

响应式设计的关键是断点选择和布局调整。1200px断点针对中等屏幕,如平板设备。768px断点针对小屏幕,如手机设备。每个断点都有对应的布局调整,确保内容在不同屏幕上都能合理显示。

4.2 CSS变量系统

应用使用CSS变量系统管理主题色彩。所有颜色值定义在根选择器的变量中,其他样式规则通过var函数引用这些变量。

:root {
    --primary: #6366f1;
    --primary-light: #818cf8;
    --primary-dark: #4f46e5;
    --secondary: #10b981;
    --accent: #f59e0b;
    --success: #10b981;
    --warning: #f59e0b;
    --danger: #ef4444;
    --info: #3b82f6;
    --bg-primary: #f8fafc;
    --bg-secondary: #ffffff;
    --bg-tertiary: #f1f5f9;
    --text-primary: #1e293b;
    --text-secondary: #475569;
    --text-muted: #94a3b8;
    --border: #e2e8f0;
}

变量系统包含多个类别。primary系列定义主色调,从浅紫到深紫形成渐变层次。secondary定义辅助色。success、warning、danger、info定义状态色,用于不同类型的提示。bg系列定义背景色,从主背景到次背景形成层次。text系列定义文字色。border定义边框色。

这种变量系统的设计思路是语义化命名。变量名不是具体的颜色值,而是用途描述。比如primary表示主色调,success表示成功状态。这种命名方式让代码更易读,也让主题切换更方便。

4.3 动画效果设计

应用在多个交互点添加了动画效果,提升用户体验。动画效果包括按钮悬停、卡片选中、Toast提示等。

.btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
}

.task-card:hover {
    background: var(--border);
}

.task-card.selected {
    border-left-color: var(--primary);
    box-shadow: var(--shadow-sm);
}

.toast {
    position: fixed;
    bottom: 30px;
    left: 50%;
    transform: translateX(-50%) translateY(100px);
    opacity: 0;
    transition: all 0.3s ease;
}

.toast.show {
    transform: translateX(-50%) translateY(0);
    opacity: 1;
}

按钮悬停动画通过transform和box-shadow实现。当鼠标悬停在按钮上时,按钮向上移动2px,同时增加阴影效果,产生浮起的视觉感受。

任务卡片悬停动画通过背景色变化实现。当鼠标悬停时,背景色变为边框色,产生高亮效果。选中卡片通过左边框颜色和阴影区分,突出显示当前选中项。

Toast提示动画通过transform和opacity实现。Toast初始状态在屏幕底部之外,透明度为零。当显示时,Toast移动到屏幕内,透明度变为1。这种动画让提示信息自然滑入,不会突兀出现。

4.4 事件处理机制

应用的事件处理采用事件委托模式。不是为每个元素单独绑定事件,而是在父容器上绑定事件,通过事件对象的target属性判断触发元素。

container.querySelectorAll('.task-card').forEach(card => {
    card.addEventListener('click', (e) => {
        if (e.target.closest('.task-action-btn')) return;
        selectTask(parseInt(card.dataset.id));
    });
});

这段代码展示了事件委托的实现方式。事件监听器绑定在任务卡片上,而不是操作按钮上。当点击事件发生时,首先检查点击目标是否是操作按钮,如果是则不处理,否则调用selectTask函数。

事件委托的优势在于动态元素支持。当列表内容动态更新时,不需要重新绑定事件,新元素自动继承事件处理。这种设计简化了代码逻辑,也避免了内存泄漏问题。

五、数据持久化与状态管理

5.1 localStorage存储机制

应用使用localStorage实现数据持久化。localStorage是浏览器提供的本地存储API,可以存储键值对数据,数据在浏览器关闭后仍然保留。

function saveState() {
    localStorage.setItem('todoState', JSON.stringify({
        tasks: appState.tasks,
        selectedTaskId: appState.selectedTaskId
    }));
}

function loadState() {
    const saved = localStorage.getItem('todoState');
    if (saved) {
        try {
            const parsed = JSON.parse(saved);
            appState = { ...appState, ...parsed };
        } catch (e) {
            console.error('加载状态失败:', e);
        }
    }
}

saveState函数将状态数据序列化为JSON字符串并存储。函数保存tasks和selectedTaskId两个属性,不保存currentFilter状态。currentFilter是UI状态,不需要持久化。

loadState函数在应用启动时读取存储数据并恢复状态。函数首先调用localStorage.getItem读取数据,然后使用JSON.parse解析字符串,最后使用对象展开运算符合并到状态对象中。try-catch块捕获可能的解析错误,防止数据格式异常导致应用崩溃。

localStorage的使用有几个注意事项。首先是数据格式,localStorage只能存储字符串,对象需要序列化。其次是数据大小,localStorage有容量限制,通常几MB,不适合存储大量数据。再次是数据安全,localStorage数据可以被用户清除,不适合存储关键数据。最后是数据同步,localStorage读写是同步操作,可能阻塞主线程。

5.2 状态对象设计

状态对象是应用的核心数据容器,所有业务数据都存储在这个对象中。状态对象采用集中管理模式,数据修改通过状态对象进行,界面渲染从状态对象读取数据。

let appState = {
    tasks: [],
    selectedTaskId: null,
    currentFilter: 'all',
    selectedCategory: null
};

状态对象包含四个主要属性。tasks存储任务数组,每个任务包含完整的属性信息。selectedTaskId存储当前选中的任务ID,用于详情显示。currentFilter存储当前的筛选条件。selectedCategory存储当前选择的分类。

这种集中式状态管理的好处是数据一致性。所有组件从同一个状态对象读取数据,避免了数据不一致的问题。数据修改也通过状态对象进行,修改后触发统一的保存和渲染流程。

状态对象的另一个好处是调试方便。开发者可以通过控制台查看状态对象,了解当前的数据状态。状态变化也可以通过日志记录,方便追踪问题。

5.3 初始数据加载

应用启动时,如果localStorage中没有数据,会自动添加示例任务,帮助用户快速体验应用功能。

if (appState.tasks.length === 0) {
    addSampleTasks();
}

function addSampleTasks() {
    const sampleTasks = [
        {
            id: Date.now() - 3,
            title: '完成项目文档',
            description: '撰写详细的技术实现文档,包括架构设计、核心功能说明和使用指南',
            priority: 'high',
            category: 'work',
            dueDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
            completed: false,
            createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString()
        },
        {
            id: Date.now() - 2,
            title: '购买生活用品',
            description: '',
            priority: 'medium',
            category: 'shopping',
            dueDate: '',
            completed: true,
            createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
        },
        {
            id: Date.now() - 1,
            title: '学习新技术',
            description: '了解最新的前端框架和工具,探索如何提升开发效率',
            priority: 'low',
            category: 'study',
            dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
            completed: false,
            createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString()
        }
    ];
    
    appState.tasks = sampleTasks;
    saveState();
    renderTaskList();
    updateTaskStats();
    showToast('已添加示例任务!');
}

这段代码展示了示例任务的添加逻辑。函数创建三个示例任务,分别代表高优先级带描述的任务、可完成的购物任务、低优先级学习任务。这些任务覆盖了不同的优先级、分类、日期等场景,帮助用户快速了解应用的各项功能。

六、快捷操作功能实现

6.1 全选和取消全选

全选功能允许用户快速选择一个任务或取消选择。

function selectAllTasks() {
    if (appState.selectedTaskId) {
        appState.selectedTaskId = null;
    } else if (appState.tasks.length > 0) {
        let filteredTasks = appState.tasks;
        if (appState.currentFilter === 'active') {
            filteredTasks = appState.tasks.filter(task => !task.completed);
        } else if (appState.currentFilter === 'completed') {
            filteredTasks = appState.tasks.filter(task => task.completed);
        }
        if (filteredTasks.length > 0) {
            appState.selectedTaskId = filteredTasks[0].id;
        }
    }
    renderTaskList();
    renderTaskDetail();
    saveState();
    showToast(appState.selectedTaskId ? '已选择任务' : '已取消选择');
}

这个函数实现了循环切换的选择逻辑。如果当前有选中项,则取消选择;如果当前没有选中项,则根据筛选条件选择第一个任务。这种设计让一个按钮实现了两个功能,简化了界面。

6.2 批量切换完成状态

批量切换功能允许用户一次切换多个任务的完成状态。

function toggleAllTasks() {
    if (appState.tasks.length === 0) {
        showToast('没有任务可操作');
        return;
    }
    
    let filteredTasks = appState.tasks;
    if (appState.currentFilter === 'active') {
        filteredTasks = appState.tasks.filter(task => !task.completed);
    } else if (appState.currentFilter === 'completed') {
        filteredTasks = appState.tasks.filter(task => task.completed);
    }
    
    if (filteredTasks.length === 0) {
        showToast('当前筛选下没有任务');
        return;
    }
    
    const allCompleted = filteredTasks.every(t => t.completed);
    filteredTasks.forEach(t => t.completed = !allCompleted);
    
    saveState();
    renderTaskList();
    if (appState.selectedTaskId) {
        renderTaskDetail();
    }
    updateTaskStats();
    updateContextInfo();
    showToast(allCompleted ? '全部标记为未完成' : '全部标记为完成');
}

这个函数实现了批量切换逻辑。首先根据筛选条件获取当前显示的任务数组。然后检查是否所有任务都已完成,如果都已完成则全部标记为未完成,否则全部标记为已完成。every方法用于检查是否所有任务都满足条件。

6.3 排序功能

排序功能允许用户按日期或优先级对任务进行排序。

function sortTasksByDate() {
    appState.tasks.sort((a, b) => {
        if (!a.dueDate) return 1;
        if (!b.dueDate) return -1;
        return new Date(a.dueDate) - new Date(b.dueDate);
    });
    saveState();
    renderTaskList();
    showToast('已按截止日期排序');
}

function sortTasksByPriority() {
    const priorityOrder = { high: 0, medium: 1, low: 2 };
    appState.tasks.sort((a, b) => {
        return priorityOrder[a.priority] - priorityOrder[b.priority];
    });
    saveState();
    renderTaskList();
    showToast('已按优先级排序');
}

按日期排序时,没有截止日期的任务排在后面。sort方法接收一个比较函数,a.dueDate为空返回1表示a排在后面,b.dueDate为空返回-1表示b排在后面。

按优先级排序时,使用priorityOrder对象定义优先级顺序。high优先级值为0,medium优先级值为1,low优先级值为2。比较时用a的优先级值减去b的优先级值,实现从高优先级到低优先级的排序。

6.4 数据导出功能

数据导出功能允许用户将任务导出为文本文件。

function exportTasks() {
    if (appState.tasks.length === 0) {
        showToast('没有任务可导出');
        return;
    }
    
    let text = 'TodoList 任务导出\n';
    text += '='.repeat(50) + '\n\n';
    
    const activeTasks = appState.tasks.filter(t => !t.completed);
    const completedTasks = appState.tasks.filter(t => t.completed);
    
    text += `【待办任务 (${activeTasks.length}项)】\n`;
    activeTasks.forEach((task, index) => {
        text += `${index + 1}. ${task.title}\n`;
        if (task.description) {
            text += `   ${task.description}\n`;
        }
        text += `   分类: ${getCategoryLabel(task.category)} | 优先级: ${getPriorityLabel(task.priority)}`;
        if (task.dueDate) {
            text += ` | 截止: ${task.dueDate}`;
        }
        text += '\n\n';
    });
    
    text += `\n【已完成任务 (${completedTasks.length}项)】\n`;
    completedTasks.forEach((task, index) => {
        text += `${index + 1}. ✓ ${task.title}\n`;
    });
    
    text += '\n' + '='.repeat(50) + '\n';
    text += '导出时间: ' + new Date().toLocaleString('zh-CN');
    
    const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const aElement = document.createElement('a');
    aElement.href = url;
    aElement.download = `todolist_${new Date().toISOString().split('T')[0]}.txt`;
    aElement.click();
    URL.revokeObjectURL(url);
    
    showToast('任务已导出!');
}

导出功能使用Blob API生成文本文件。文件内容分为待办任务和已完成任务两部分,每部分列出任务编号、标题、描述、分类、优先级和截止日期。文件使用UTF-8编码,确保中文内容正确显示。

导出流程:创建Blob对象 → 生成临时URL → 创建下载链接 → 触发点击 → 释放URL资源。文件名包含当前日期,方便用户识别。

七、实时时钟功能

7.1 时钟启动

应用启动时,启动实时时钟,每秒更新一次上下文信息。

function startClock() {
    setInterval(updateContextInfo, 1000);
}

startClock函数使用setInterval启动定时器,每隔1000毫秒(1秒)调用一次updateContextInfo函数。这种设计确保上下文信息始终显示最新时间,用户可以直观地看到当前时间。

7.2 时间格式化

时间格式化使用JavaScript的LocaleString方法,支持中文显示。

const timeStr = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
const dateStr = now.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' });

LocaleString方法接受两个参数:语言标签和格式化选项。'zh-CN’指定中文语言环境,格式化选项指定要显示的时间部分。时间显示小时和分钟,日期显示年、月、日和星期。

这种格式化方式的优势是自动处理本地化,不需要手动计算月份名称和星期名称。JavaScript引擎会根据系统语言环境选择合适的格式。

八、字符计数功能

8.1 输入框字符计数

字符计数功能实时显示输入框中的字符数量。

function updateCharCount(inputId, countId, maxLength) {
    const input = document.getElementById(inputId);
    const count = document.getElementById(countId);
    count.textContent = `${input.value.length}/${maxLength}`;
    if (input.value.length > maxLength) {
        count.style.color = 'var(--danger)';
    } else {
        count.style.color = 'var(--text-muted)';
    }
}

updateCharCount函数读取输入框的当前长度,显示"当前长度/最大长度"的格式。如果长度超过最大限制,颜色变为警告色,提醒用户超出限制。

8.2 事件绑定

字符计数通过input事件实时更新。

document.getElementById('taskTitle').addEventListener('input', () => {
    updateCharCount('taskTitle', 'titleCharCount', 100);
});

input事件在用户输入时触发,比change事件更及时,能够实时反映输入内容的变化。这种设计让用户随时知道输入的长度,避免超过限制。

九、Electron与OpenHarmony融合框架

9.1 web_engine模块架构

web_engine模块是Electron与OpenHarmony融合的核心组件。这个模块实现了Web应用与鸿蒙原生环境的桥接,让HTML5应用能够运行在鸿蒙桌面环境中。

web_engine模块的架构分为三层。顶层是JavaScript绑定层,提供Web API接口。中层是适配器层,实现鸿蒙原生功能调用。底层是原生库层,提供底层能力支持。

JavaScript绑定层定义了Web应用可用的API接口。这些接口包括窗口管理、文件操作、网络请求、设备访问等。Web应用通过这些接口调用鸿蒙原生功能,实现跨平台能力。

适配器层实现了JavaScript接口与鸿蒙原生API的映射。每个适配器对应一个功能领域,如AppWindowAdapter处理窗口操作,FileManagerAdapter处理文件操作。适配器将JavaScript调用转换为鸿蒙API调用,处理参数转换和返回值封装。

原生库层提供了底层能力支持。libadapter.so是核心原生库,实现了Electron运行时的底层功能。这个库处理进程通信、窗口渲染、事件传递等核心操作。

9.2 应用启动流程

应用的启动流程涉及多个模块的协作。从用户点击应用图标到界面显示,经历了一系列初始化步骤。

首先是Ability启动。鸿蒙系统创建EntryAbility实例,调用onCreate方法初始化能力。EntryAbility继承自WebAbility,WebAbility继承自WebBaseAbility,形成能力继承链。

其次是窗口创建。WebBaseAbility的onWindowStageCreate方法创建应用窗口。窗口创建后,加载Index页面,Index页面包含WebWindow组件。

然后是WebWindow初始化。WebWindow组件创建WindowNodeController,WindowNodeController创建XComponent节点。XComponent是渲染载体,承载Web内容的显示。

最后是Web内容加载。XComponent的onLoad回调触发,调用nativeContext.runBrowser方法启动Electron运行时。Electron运行时加载main.js,main.js创建BrowserWindow并加载HTML应用。

这个启动流程的优势是分层清晰、职责明确。每个模块负责特定的功能,模块之间通过接口协作。这种设计让代码结构清晰,便于维护和扩展。

十、性能优化与最佳实践

10.1 渲染性能优化

界面渲染是应用性能的关键因素。应用通过几个策略优化渲染性能。

首先是批量更新。当状态数据变化时,不是立即更新DOM,而是调用对应的渲染函数。渲染函数使用innerHTML一次性更新整个列表,而不是逐个添加元素。

其次是条件渲染。任务列表为空时显示空状态,而不是显示空白的列表。这种设计避免了不必要的DOM节点创建。

再次是模板复用。任务卡片的HTML结构使用模板字符串定义,在map方法中复用。这种设计减少了代码重复,提高了可维护性。

最后是事件委托。列表项的事件监听器绑定在父容器上,而不是每个子元素上。这种设计减少了事件监听器的数量,提高了事件处理效率。

10.2 内存管理策略

内存管理是长期运行应用的重要考虑。应用通过几个策略管理内存。

首先是状态对象管理。状态对象只保存必要的数据,不保存大型数据结构。任务列表数组是必需的数据,其他状态如currentFilter是轻量级值。

其次是DOM引用缓存。频繁访问的DOM元素没有缓存,每次访问都重新查询。这种设计简化了代码,对于少量元素影响不大。

再次是定时器清理。虽然应用没有显式清理定时器,但页面关闭时定时器会自动停止。setInterval在页面卸载时会自动清理。

最后是Blob资源释放。文件导出后,调用revokeObjectURL释放Blob URL资源。如果不释放,URL资源会继续占用内存。

10.3 代码组织最佳实践

代码组织影响代码的可读性和可维护性。应用采用几个最佳实践组织代码。

首先是模块化设计。每个功能模块独立实现,模块之间通过函数调用协作。比如任务管理模块、上下文感知模块、数据持久化模块各自独立。

其次是函数单一职责。每个函数只做一件事,职责清晰。比如renderTaskList只负责渲染列表,handleAddTask只负责添加任务。

再次是命名规范。变量和函数使用语义化命名。比如updateCharCount表示更新字符计数,getDueDateInfo表示获取截止日期信息。

最后是注释适度。关键逻辑添加注释说明,简单代码不加注释。这种平衡让代码既清晰又不冗余。

十一、用户体验设计思考

11.1 信息架构设计

信息架构决定了用户如何浏览和理解应用内容。应用的信息架构基于任务管理流程设计,分为任务创建、任务查看、任务操作三个阶段。

任务创建阶段包含标题输入、描述输入、优先级选择、截止日期选择、分类选择五个步骤。这种分步设计让创建过程清晰,不会让用户感到信息过载。

任务查看阶段包含任务列表查看、任务详情查看、上下文信息查看三个视角。用户可以快速浏览任务列表,点击查看详情,同时参考上下文信息做出决策。

任务操作阶段包含完成切换、删除、编辑三个操作。操作按钮设计在任务卡片和详情面板中,方便用户随时执行。

这种信息架构的好处是流程清晰、步骤明确。用户能够按照自然流程使用应用,从创建任务到查看任务再到操作任务,形成完整的管理闭环。

11.2 视觉层次设计

视觉层次设计帮助用户快速理解页面结构,找到所需的功能。

页面分为左右两栏,左栏侧重任务输入和列表,右栏侧重任务详情和上下文。这种对称布局符合用户的阅读习惯,大脑可以自然地将页面分为操作区和信息区。

任务卡片采用卡片式设计,每个任务独立一块。卡片内部有清晰的层次结构,标题突出显示,元信息次要显示,操作按钮再次显示。这种层次设计让用户能够快速扫描定位。

优先级使用颜色徽章标识,高优先级红色、中优先级黄色、低优先级绿色。这种颜色编码符合直觉,用户一眼就能识别优先级高低。

11.3 错误处理策略

错误处理影响应用的稳定性。应用采用几个策略处理可能出现的错误。

首先是输入验证。任务标题必填,如果为空则显示提示并阻止添加。输入验证在客户端执行,即时反馈,用户体验好。

其次是确认机制。删除任务时弹出确认对话框,防止误删。confirm是浏览器原生的确认对话框,不需要额外实现。

再次是异常捕获。localStorage操作使用try-catch捕获异常,防止数据格式异常导致崩溃。异常发生时只是加载默认状态,不会导致应用崩溃。

最后是边界检查。数组操作前检查长度,日期计算前检查有效性。这些边界检查避免了运行时错误。

11.4 反馈机制设计

反馈机制让用户感知操作的响应。应用在多个交互点添加反馈效果。

首先是Toast提示。添加任务成功、删除任务、导出任务等操作完成后显示Toast提示。Toast在屏幕底部居中显示,几秒后自动消失,不干扰用户操作。

其次是视觉反馈。按钮悬停时变色和浮起,任务卡片悬停时高亮,选中任务时边框突出。这些视觉变化让用户知道操作已被识别。

再次是状态反馈。任务完成时文字加删除线,紧急任务截止日期高亮显示。这种状态反馈让用户快速了解任务情况。

最后是进度反馈。上下文信息实时更新,用户可以看到当前时间、完成进度、紧急任务数量。这种持续反馈帮助用户做出决策。

十二、未来扩展方向

12.1 功能扩展计划

应用有多个功能扩展方向,可以进一步提升实用性。

首先是任务编辑功能。当前版本只支持添加和删除,不支持编辑。添加编辑功能允许用户修改已创建的任务,完善任务管理能力。

其次是任务分组功能。当前版本只有分类标签,没有分组功能。添加分组功能允许用户将任务组织到不同的组中,比如按项目分组、按时间分组。

再次是提醒功能。当前版本只有截止日期显示,没有提醒功能。添加提醒功能允许用户设置提醒时间,系统在指定时间通知用户。

最后是数据同步功能。当前版本只有本地存储,没有云同步。添加数据同步功能允许用户在多个设备间同步任务数据。

12.2 技术升级方向

技术层面也有多个升级方向,可以提升应用性能和体验。

首先是框架升级。当前应用使用原生JavaScript,可以升级到Vue或React框架。框架提供了组件化开发、状态管理、路由控制等能力,能够简化开发流程。

其次是存储升级。当前使用localStorage,可以升级到IndexedDB。IndexedDB支持更大存储容量和更复杂查询,适合存储大量任务数据。

再次是样式升级。当前使用手写CSS,可以升级到Tailwind CSS或Styled Components。CSS框架提供了丰富的样式类和主题系统,能够加速UI开发。

最后是测试升级。当前版本没有自动化测试,可以添加单元测试和集成测试。测试覆盖关键函数,确保代码质量。

十三、总结与展望

TodoList 任务详情子系统是Electron与OpenHarmony融合框架的一次成功实践。应用通过数字化手段解决了任务管理中的实际问题,提供了可选描述字段、上下文感知渲染、分类管理、优先级设置等实用功能。技术实现采用Web技术栈,代码结构清晰,维护方便。

应用的核心价值在于实用性和智能化。实用性体现在功能设计围绕任务管理流程,每个功能都解决实际问题。智能化体现在上下文感知渲染,根据当前状态智能显示相关信息,帮助用户更高效地管理任务。

技术层面,应用展示了Web技术在桌面应用中的应用潜力。通过web_engine模块,Web应用能够运行在鸿蒙桌面环境中,享受原生平台的性能和功能。这种融合框架降低了跨平台开发的门槛,让更多开发者能够参与鸿蒙生态建设。

数据层面,应用展示了localStorage在本地存储中的应用。通过状态对象集中管理数据,实现了数据的持久化存储。这种存储方式简单可靠,适合小型应用。

交互层面,应用展示了现代Web应用的交互设计。通过Toast提示、视觉反馈、状态反馈等机制,提升了用户体验。这种交互设计让应用更加友好,用户愿意长期使用。

展望未来,应用有广阔的扩展空间。功能层面可以添加任务编辑、任务分组、提醒功能、数据同步等。技术层面可以升级框架、存储、样式、测试等。这些扩展将进一步提升应用的价值,服务更多用户。

最后,感谢开源鸿蒙PC社区的支持。社区提供了技术交流平台,让开发者能够分享经验、解决问题。希望这个应用能够为社区贡献一份力量,推动鸿蒙生态的发展。


技术要点总结:

技术点 实现方式 关键代码
任务数据结构 对象数组 appState.tasks,包含id、title、description等属性
上下文感知 时间计算 updateContextInfo函数,计算实时信息和紧急任务
截止日期判断 日期差计算 getDueDateInfo函数,判断今天/明天/已过期等
任务筛选 数组过滤 filter方法,根据completed状态筛选
数据持久化 localStorage saveState和loadState函数
字符计数 input事件 updateCharCount函数,实时显示输入长度
排序功能 sort方法 sortTasksByDate和sortTasksByPriority函数
数据导出 Blob API exportTasks函数,生成文本文件
实时时钟 setInterval startClock函数,每秒更新
事件委托 父容器监听 click事件,检测目标元素

核心代码文件:

  1. [todolist_detail.html](file:///d:/save/systemIso/electron-openharmony-vue3/ohos_hap/web_engine/src/main/resources/resfile/resources/app/todolist_detail.html) - 页面结构和组件定义
  2. [todolist_detail_style.css](file:///d:/save/systemIso/electron-openharmony-vue3/ohos_hap/web_engine/src/main/resources/resfile/resources/app/todolist_detail_style.css) - 样式系统和动画效果
  3. [todolist_detail_app.js](file:///d:/save/systemIso/electron-openharmony-vue3/ohos_hap/web_engine/src/main/resources/resfile/resources/app/js/todolist_detail_app.js) - 核心逻辑和状态管理
  4. [main.js](file:///d:/save/systemIso/electron-openharmony-vue3/ohos_hap/web_engine/src/main/resources/resfile/resources/app/main.js) - Electron入口和窗口配置

参考资料:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐