实战项目:待办事项小程序(中)- 功能实现和数据处理
🚀 在上一篇的基础上,我们继续完善待办事项小程序的核心功能
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状态管理模块
- ✅ 任务列表核心组件
- ✅ 筛选、排序、分页功能
核心要点:
- 数据模型要考虑扩展性
- 存储服务要处理异常情况
- 状态管理要保持数据一致性
- 组件要职责单一、可复用
下一篇预告
下一篇我们将学习《实战项目:待办事项小程序(下)- 高级功能和优化》,完成剩余功能并进行性能优化。
功能实现是项目的核心,良好的架构设计让后续开发更加顺畅!