Skip to content

实战项目:待办事项小程序(中)- 功能实现和数据处理

🚀 在上一篇的基础上,我们继续完善待办事项小程序的核心功能

1. 数据模型设计

任务数据结构

javascript
// models/task.js
export class Task {
  constructor(data = {}) {
    this.id = data.id || this.generateId()
    this.title = data.title || ''
    this.description = data.description || ''
    this.completed = data.completed || false
    this.priority = data.priority || 'medium' // low, medium, high
    this.category = data.category || 'default'
    this.dueDate = data.dueDate || null
    this.createdAt = data.createdAt || new Date().toISOString()
    this.updatedAt = data.updatedAt || new Date().toISOString()
    this.tags = data.tags || []
    this.reminder = data.reminder || null
  }
  
  generateId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2)
  }
  
  // 更新任务
  update(data) {
    Object.assign(this, data)
    this.updatedAt = new Date().toISOString()
    return this
  }
  
  // 切换完成状态
  toggle() {
    this.completed = !this.completed
    this.updatedAt = new Date().toISOString()
    return this
  }
  
  // 是否过期
  isOverdue() {
    if (!this.dueDate || this.completed) return false
    return new Date(this.dueDate) < new Date()
  }
  
  // 是否今天到期
  isDueToday() {
    if (!this.dueDate) return false
    const today = new Date().toDateString()
    const dueDate = new Date(this.dueDate).toDateString()
    return today === dueDate
  }
  
  // 序列化为存储格式
  toJSON() {
    return {
      id: this.id,
      title: this.title,
      description: this.description,
      completed: this.completed,
      priority: this.priority,
      category: this.category,
      dueDate: this.dueDate,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      tags: this.tags,
      reminder: this.reminder
    }
  }
}

分类数据结构

javascript
// models/category.js
export class Category {
  constructor(data = {}) {
    this.id = data.id || this.generateId()
    this.name = data.name || ''
    this.color = data.color || '#007aff'
    this.icon = data.icon || '📝'
    this.createdAt = data.createdAt || new Date().toISOString()
  }
  
  generateId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2)
  }
  
  toJSON() {
    return {
      id: this.id,
      name: this.name,
      color: this.color,
      icon: this.icon,
      createdAt: this.createdAt
    }
  }
}

2. 数据存储服务

本地存储服务

javascript
// services/storage.js
class StorageService {
  constructor() {
    this.TASKS_KEY = 'todo_tasks'
    this.CATEGORIES_KEY = 'todo_categories'
    this.SETTINGS_KEY = 'todo_settings'
  }
  
  // 任务相关
  async getTasks() {
    try {
      const tasks = uni.getStorageSync(this.TASKS_KEY) || []
      return tasks.map(task => new Task(task))
    } catch (error) {
      console.error('获取任务失败:', error)
      return []
    }
  }
  
  async saveTasks(tasks) {
    try {
      const tasksData = tasks.map(task => task.toJSON())
      uni.setStorageSync(this.TASKS_KEY, tasksData)
      return true
    } catch (error) {
      console.error('保存任务失败:', error)
      return false
    }
  }
  
  async addTask(task) {
    const tasks = await this.getTasks()
    tasks.push(task)
    return this.saveTasks(tasks)
  }
  
  async updateTask(taskId, updates) {
    const tasks = await this.getTasks()
    const taskIndex = tasks.findIndex(task => task.id === taskId)
    
    if (taskIndex !== -1) {
      tasks[taskIndex].update(updates)
      return this.saveTasks(tasks)
    }
    
    return false
  }
  
  async deleteTask(taskId) {
    const tasks = await this.getTasks()
    const filteredTasks = tasks.filter(task => task.id !== taskId)
    return this.saveTasks(filteredTasks)
  }
  
  // 分类相关
  async getCategories() {
    try {
      const categories = uni.getStorageSync(this.CATEGORIES_KEY) || this.getDefaultCategories()
      return categories.map(cat => new Category(cat))
    } catch (error) {
      console.error('获取分类失败:', error)
      return this.getDefaultCategories()
    }
  }
  
  async saveCategories(categories) {
    try {
      const categoriesData = categories.map(cat => cat.toJSON())
      uni.setStorageSync(this.CATEGORIES_KEY, categoriesData)
      return true
    } catch (error) {
      console.error('保存分类失败:', error)
      return false
    }
  }
  
  getDefaultCategories() {
    return [
      new Category({ name: '工作', color: '#007aff', icon: '💼' }),
      new Category({ name: '生活', color: '#34c759', icon: '🏠' }),
      new Category({ name: '学习', color: '#ff9500', icon: '📚' }),
      new Category({ name: '健康', color: '#ff3b30', icon: '💪' })
    ]
  }
  
  // 设置相关
  async getSettings() {
    try {
      return uni.getStorageSync(this.SETTINGS_KEY) || this.getDefaultSettings()
    } catch (error) {
      console.error('获取设置失败:', error)
      return this.getDefaultSettings()
    }
  }
  
  async saveSettings(settings) {
    try {
      uni.setStorageSync(this.SETTINGS_KEY, settings)
      return true
    } catch (error) {
      console.error('保存设置失败:', error)
      return false
    }
  }
  
  getDefaultSettings() {
    return {
      theme: 'light',
      notifications: true,
      soundEnabled: true,
      defaultPriority: 'medium',
      autoSort: true
    }
  }
  
  // 数据导出
  async exportData() {
    const tasks = await this.getTasks()
    const categories = await this.getCategories()
    const settings = await this.getSettings()
    
    return {
      tasks: tasks.map(task => task.toJSON()),
      categories: categories.map(cat => cat.toJSON()),
      settings,
      exportDate: new Date().toISOString(),
      version: '1.0'
    }
  }
  
  // 数据导入
  async importData(data) {
    try {
      if (data.tasks) {
        const tasks = data.tasks.map(task => new Task(task))
        await this.saveTasks(tasks)
      }
      
      if (data.categories) {
        const categories = data.categories.map(cat => new Category(cat))
        await this.saveCategories(categories)
      }
      
      if (data.settings) {
        await this.saveSettings(data.settings)
      }
      
      return true
    } catch (error) {
      console.error('导入数据失败:', error)
      return false
    }
  }
}

export default new StorageService()

3. 状态管理

Vuex Store模块

javascript
// store/modules/tasks.js
import { Task } from '@/models/task.js'
import storageService from '@/services/storage.js'

const tasks = {
  namespaced: true,
  
  state: {
    tasks: [],
    categories: [],
    currentFilter: 'all', // all, active, completed
    currentCategory: null,
    searchKeyword: '',
    sortBy: 'createdAt', // createdAt, dueDate, priority, title
    sortOrder: 'desc' // asc, desc
  },
  
  mutations: {
    SET_TASKS(state, tasks) {
      state.tasks = tasks
    },
    
    SET_CATEGORIES(state, categories) {
      state.categories = categories
    },
    
    ADD_TASK(state, task) {
      state.tasks.unshift(task)
    },
    
    UPDATE_TASK(state, { taskId, updates }) {
      const taskIndex = state.tasks.findIndex(task => task.id === taskId)
      if (taskIndex !== -1) {
        state.tasks[taskIndex].update(updates)
      }
    },
    
    DELETE_TASK(state, taskId) {
      state.tasks = state.tasks.filter(task => task.id !== taskId)
    },
    
    TOGGLE_TASK(state, taskId) {
      const task = state.tasks.find(task => task.id === taskId)
      if (task) {
        task.toggle()
      }
    },
    
    SET_FILTER(state, filter) {
      state.currentFilter = filter
    },
    
    SET_CATEGORY_FILTER(state, categoryId) {
      state.currentCategory = categoryId
    },
    
    SET_SEARCH_KEYWORD(state, keyword) {
      state.searchKeyword = keyword
    },
    
    SET_SORT(state, { sortBy, sortOrder }) {
      state.sortBy = sortBy
      state.sortOrder = sortOrder
    }
  },
  
  actions: {
    async loadTasks({ commit }) {
      try {
        const tasks = await storageService.getTasks()
        const categories = await storageService.getCategories()
        
        commit('SET_TASKS', tasks)
        commit('SET_CATEGORIES', categories)
        
        return { tasks, categories }
      } catch (error) {
        console.error('加载任务失败:', error)
        throw error
      }
    },
    
    async addTask({ commit, state }, taskData) {
      try {
        const task = new Task(taskData)
        await storageService.addTask(task)
        
        commit('ADD_TASK', task)
        
        uni.showToast({
          title: '任务添加成功',
          icon: 'success'
        })
        
        return task
      } catch (error) {
        console.error('添加任务失败:', error)
        uni.showToast({
          title: '添加失败',
          icon: 'error'
        })
        throw error
      }
    },
    
    async updateTask({ commit }, { taskId, updates }) {
      try {
        await storageService.updateTask(taskId, updates)
        commit('UPDATE_TASK', { taskId, updates })
        
        return true
      } catch (error) {
        console.error('更新任务失败:', error)
        throw error
      }
    },
    
    async deleteTask({ commit }, taskId) {
      try {
        await storageService.deleteTask(taskId)
        commit('DELETE_TASK', taskId)
        
        uni.showToast({
          title: '任务已删除',
          icon: 'success'
        })
        
        return true
      } catch (error) {
        console.error('删除任务失败:', error)
        uni.showToast({
          title: '删除失败',
          icon: 'error'
        })
        throw error
      }
    },
    
    async toggleTask({ commit }, taskId) {
      try {
        const task = this.getters['tasks/getTaskById'](taskId)
        if (task) {
          const updates = { completed: !task.completed }
          await storageService.updateTask(taskId, updates)
          commit('TOGGLE_TASK', taskId)
        }
        
        return true
      } catch (error) {
        console.error('切换任务状态失败:', error)
        throw error
      }
    },
    
    setFilter({ commit }, filter) {
      commit('SET_FILTER', filter)
    },
    
    setCategoryFilter({ commit }, categoryId) {
      commit('SET_CATEGORY_FILTER', categoryId)
    },
    
    setSearchKeyword({ commit }, keyword) {
      commit('SET_SEARCH_KEYWORD', keyword)
    },
    
    setSort({ commit }, sortConfig) {
      commit('SET_SORT', sortConfig)
    }
  },
  
  getters: {
    // 获取过滤后的任务
    filteredTasks: (state, getters) => {
      let tasks = [...state.tasks]
      
      // 状态过滤
      if (state.currentFilter === 'active') {
        tasks = tasks.filter(task => !task.completed)
      } else if (state.currentFilter === 'completed') {
        tasks = tasks.filter(task => task.completed)
      }
      
      // 分类过滤
      if (state.currentCategory) {
        tasks = tasks.filter(task => task.category === state.currentCategory)
      }
      
      // 搜索过滤
      if (state.searchKeyword) {
        const keyword = state.searchKeyword.toLowerCase()
        tasks = tasks.filter(task => 
          task.title.toLowerCase().includes(keyword) ||
          task.description.toLowerCase().includes(keyword) ||
          task.tags.some(tag => tag.toLowerCase().includes(keyword))
        )
      }
      
      // 排序
      tasks.sort((a, b) => {
        let aValue = a[state.sortBy]
        let bValue = b[state.sortBy]
        
        // 特殊处理优先级排序
        if (state.sortBy === 'priority') {
          const priorityOrder = { high: 3, medium: 2, low: 1 }
          aValue = priorityOrder[aValue]
          bValue = priorityOrder[bValue]
        }
        
        // 特殊处理日期排序
        if (state.sortBy === 'dueDate') {
          aValue = aValue ? new Date(aValue) : new Date('9999-12-31')
          bValue = bValue ? new Date(bValue) : new Date('9999-12-31')
        }
        
        if (state.sortOrder === 'asc') {
          return aValue > bValue ? 1 : -1
        } else {
          return aValue < bValue ? 1 : -1
        }
      })
      
      return tasks
    },
    
    // 获取任务统计
    taskStats: (state) => {
      const total = state.tasks.length
      const completed = state.tasks.filter(task => task.completed).length
      const active = total - completed
      const overdue = state.tasks.filter(task => task.isOverdue()).length
      const dueToday = state.tasks.filter(task => task.isDueToday()).length
      
      return {
        total,
        completed,
        active,
        overdue,
        dueToday,
        completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
      }
    },
    
    // 根据分类分组任务
    tasksByCategory: (state) => {
      const groups = {}
      
      state.categories.forEach(category => {
        groups[category.id] = {
          category,
          tasks: state.tasks.filter(task => task.category === category.id)
        }
      })
      
      return groups
    },
    
    // 获取单个任务
    getTaskById: (state) => (id) => {
      return state.tasks.find(task => task.id === id)
    },
    
    // 获取分类
    getCategoryById: (state) => (id) => {
      return state.categories.find(category => category.id === id)
    }
  }
}

export default tasks

4. 核心功能组件

任务列表组件

vue
<!-- components/TaskList.vue -->
<template>
  <view class="task-list">
    <!-- 筛选和排序 -->
    <view class="list-header">
      <view class="filter-tabs">
        <view 
          v-for="filter in filters"
          :key="filter.value"
          class="filter-tab"
          :class="{ active: currentFilter === filter.value }"
          @click="setFilter(filter.value)"
        >
          <text>\{\{ filter.label \}\}</text>
          <view v-if="filter.count !== undefined" class="count-badge">
            \{\{ filter.count \}\}
          </view>
        </view>
      </view>
      
      <view class="sort-controls">
        <picker 
          :range="sortOptions"
          range-key="label"
          :value="sortIndex"
          @change="onSortChange"
        >
          <view class="sort-picker">
            <text>\{\{ currentSortLabel \}\}</text>
            <text class="sort-icon">⌄</text>
          </view>
        </picker>
      </view>
    </view>
    
    <!-- 任务列表 -->
    <scroll-view 
      scroll-y 
      class="task-scroll"
      @scrolltolower="onScrollToLower"
    >
      <view v-if="filteredTasks.length === 0" class="empty-state">
        <text class="empty-icon">📝</text>
        <text class="empty-text">\{\{ emptyText \}\}</text>
      </view>
      
      <view v-else class="tasks">
        <task-item
          v-for="task in paginatedTasks"
          :key="task.id"
          :task="task"
          @toggle="onToggleTask"
          @edit="onEditTask"
          @delete="onDeleteTask"
        />
        
        <!-- 加载更多 -->
        <view v-if="hasMore" class="load-more">
          <text>加载更多...</text>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex'
import TaskItem from './TaskItem.vue'

export default {
  name: 'TaskList',
  components: {
    TaskItem
  },
  
  data() {
    return {
      pageSize: 20,
      currentPage: 1,
      sortOptions: [
        { value: 'createdAt', label: '创建时间', order: 'desc' },
        { value: 'dueDate', label: '截止时间', order: 'asc' },
        { value: 'priority', label: '优先级', order: 'desc' },
        { value: 'title', label: '标题', order: 'asc' }
      ]
    }
  },
  
  computed: {
    ...mapState('tasks', ['currentFilter', 'sortBy', 'sortOrder']),
    ...mapGetters('tasks', ['filteredTasks', 'taskStats']),
    
    filters() {
      return [
        { value: 'all', label: '全部', count: this.taskStats.total },
        { value: 'active', label: '进行中', count: this.taskStats.active },
        { value: 'completed', label: '已完成', count: this.taskStats.completed }
      ]
    },
    
    sortIndex() {
      return this.sortOptions.findIndex(option => 
        option.value === this.sortBy && option.order === this.sortOrder
      )
    },
    
    currentSortLabel() {
      const option = this.sortOptions[this.sortIndex]
      return option ? option.label : '排序'
    },
    
    paginatedTasks() {
      const endIndex = this.currentPage * this.pageSize
      return this.filteredTasks.slice(0, endIndex)
    },
    
    hasMore() {
      return this.paginatedTasks.length < this.filteredTasks.length
    },
    
    emptyText() {
      if (this.currentFilter === 'active') {
        return '暂无进行中的任务'
      } else if (this.currentFilter === 'completed') {
        return '暂无已完成的任务'
      } else {
        return '暂无任务,点击添加按钮创建第一个任务吧'
      }
    }
  },
  
  methods: {
    ...mapActions('tasks', ['setFilter', 'setSort', 'toggleTask', 'deleteTask']),
    
    onSortChange(e) {
      const option = this.sortOptions[e.detail.value]
      if (option) {
        this.setSort({
          sortBy: option.value,
          sortOrder: option.order
        })
      }
    },
    
    onScrollToLower() {
      if (this.hasMore) {
        this.currentPage++
      }
    },
    
    async onToggleTask(taskId) {
      try {
        await this.toggleTask(taskId)
      } catch (error) {
        console.error('切换任务状态失败:', error)
      }
    },
    
    onEditTask(task) {
      this.$emit('edit', task)
    },
    
    async onDeleteTask(taskId) {
      try {
        await uni.showModal({
          title: '确认删除',
          content: '确定要删除这个任务吗?',
          confirmText: '删除',
          confirmColor: '#ff3b30'
        })
        
        await this.deleteTask(taskId)
      } catch (error) {
        if (error.confirm !== false) {
          console.error('删除任务失败:', error)
        }
      }
    }
  },
  
  watch: {
    currentFilter() {
      this.currentPage = 1
    },
    
    filteredTasks() {
      this.currentPage = 1
    }
  }
}
</script>

<style lang="scss">
.task-list {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.list-header {
  background: white;
  padding: 20rpx;
  border-bottom: 1rpx solid #f0f0f0;
}

.filter-tabs {
  display: flex;
  margin-bottom: 20rpx;
}

.filter-tab {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16rpx;
  border-radius: 8rpx;
  background: #f8f9fa;
  margin-right: 10rpx;
  
  &:last-child {
    margin-right: 0;
  }
  
  &.active {
    background: #007aff;
    color: white;
    
    .count-badge {
      background: rgba(255, 255, 255, 0.3);
    }
  }
}

.count-badge {
  background: #e9ecef;
  color: #666;
  padding: 4rpx 8rpx;
  border-radius: 12rpx;
  font-size: 20rpx;
  margin-left: 8rpx;
  min-width: 32rpx;
  text-align: center;
}

.sort-controls {
  display: flex;
  justify-content: flex-end;
}

.sort-picker {
  display: flex;
  align-items: center;
  padding: 12rpx 16rpx;
  background: #f8f9fa;
  border-radius: 8rpx;
  font-size: 26rpx;
  color: #666;
}

.sort-icon {
  margin-left: 8rpx;
  font-size: 20rpx;
}

.task-scroll {
  flex: 1;
  background: #f8f9fa;
}

.empty-state {
  text-align: center;
  padding: 100rpx 40rpx;
}

.empty-icon {
  font-size: 120rpx;
  display: block;
  margin-bottom: 30rpx;
  opacity: 0.3;
}

.empty-text {
  color: #999;
  font-size: 28rpx;
  line-height: 1.5;
}

.tasks {
  padding: 20rpx;
}

.load-more {
  text-align: center;
  padding: 30rpx;
  color: #999;
  font-size: 26rpx;
}
</style>

小结

在这一篇中,我们实现了:

  • ✅ 完整的数据模型设计
  • ✅ 本地存储服务封装
  • ✅ Vuex状态管理模块
  • ✅ 任务列表核心组件
  • ✅ 筛选、排序、分页功能

核心要点

  • 数据模型要考虑扩展性
  • 存储服务要处理异常情况
  • 状态管理要保持数据一致性
  • 组件要职责单一、可复用

下一篇预告

下一篇我们将学习《实战项目:待办事项小程序(下)- 高级功能和优化》,完成剩余功能并进行性能优化。


功能实现是项目的核心,良好的架构设计让后续开发更加顺畅!