Tailwind CSSTailwind CSS
Home
  • Tailwind CSS 书籍目录
  • Vue 3 开发实战指南
  • React 和 Next.js 学习
  • TypeScript
  • React开发框架书籍大纲
  • Shadcn学习大纲
  • Swift 编程语言:从入门到进阶
  • SwiftUI 学习指南
  • 函数式编程大纲
  • Swift 异步编程语言
  • Swift 协议化编程
  • SwiftUI MVVM 开发模式
  • SwiftUI 图表开发书籍
  • SwiftData
  • ArkTS编程语言:从入门到精通
  • 仓颉编程语言:从入门到精通
  • 鸿蒙手机客户端开发实战
  • WPF书籍
  • C#开发书籍
learn
  • Java编程语言
  • Kotlin 编程入门与实战
  • /python/outline.html
  • AI Agent
  • MCP (Model Context Protocol) 应用指南
  • 深度学习
  • 深度学习
  • 强化学习: 理论与实践
  • 扩散模型书籍
  • Agentic AI for Everyone
langchain
Home
  • Tailwind CSS 书籍目录
  • Vue 3 开发实战指南
  • React 和 Next.js 学习
  • TypeScript
  • React开发框架书籍大纲
  • Shadcn学习大纲
  • Swift 编程语言:从入门到进阶
  • SwiftUI 学习指南
  • 函数式编程大纲
  • Swift 异步编程语言
  • Swift 协议化编程
  • SwiftUI MVVM 开发模式
  • SwiftUI 图表开发书籍
  • SwiftData
  • ArkTS编程语言:从入门到精通
  • 仓颉编程语言:从入门到精通
  • 鸿蒙手机客户端开发实战
  • WPF书籍
  • C#开发书籍
learn
  • Java编程语言
  • Kotlin 编程入门与实战
  • /python/outline.html
  • AI Agent
  • MCP (Model Context Protocol) 应用指南
  • 深度学习
  • 深度学习
  • 强化学习: 理论与实践
  • 扩散模型书籍
  • Agentic AI for Everyone
langchain
  • 优化用户体验与性能

优化用户体验与性能

在完成了 TaskMaster 的任务列表、状态管理和编辑排序功能后,本节将聚焦于优化用户体验(UX)和性能。通过改进交互反馈、添加输入验证、优化列表渲染和减少不必要更新,我们将提升应用的可用性和效率。本节将展示具体的优化策略和实现方法,帮助你打造一个更优质的 Vue 3 应用。

优化目标

用户体验

  • 输入验证:确保任务标题非空,日期有效。
  • 交互反馈:优化加载状态和操作提示。
  • 样式美化:改进视觉效果,提升可读性。

性能

  • 列表渲染:使用虚拟滚动优化大型任务列表。
  • 响应式优化:减少不必要的追踪和渲染。

编码实现

1. 更新 Pinia Store

stores/task.ts

// src/stores/task.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

interface Task {
  id: number;
  title: string;
  description: string;
  status: 'todo' | 'in-progress' | 'done';
  dueDate: string;
}

export const useTaskStore = defineStore('task', () => {
  const tasks = ref<Task[]>([]);
  const isLoading = ref(false);

  const loadTasks = () => {
    isLoading.value = true;
    const savedTasks = localStorage.getItem('tasks');
    if (savedTasks) tasks.value = JSON.parse(savedTasks);
    isLoading.value = false;
  };

  const saveTasks = () => {
    localStorage.setItem('tasks', JSON.stringify(tasks.value));
  };

  const addTask = (task: Omit<Task, 'id'>): boolean => {
    if (!task.title.trim()) return false;
    tasks.value.push({ id: Date.now(), ...task });
    saveTasks();
    return true;
  };

  const updateTask = (id: number, updates: Partial<Task>): boolean => {
    if (updates.title && !updates.title.trim()) return false;
    const index = tasks.value.findIndex(t => t.id === id);
    if (index !== -1) {
      tasks.value[index] = { ...tasks.value[index], ...updates };
      saveTasks();
      return true;
    }
    return false;
  };

  const deleteTask = (id: number) => {
    tasks.value = tasks.value.filter(t => t.id !== id);
    saveTasks();
  };

  const sortTasksByDueDate = (ascending: boolean = true) => {
    tasks.value.sort((a, b) => {
      const dateA = new Date(a.dueDate).getTime();
      const dateB = new Date(b.dueDate).getTime();
      return ascending ? dateA - dateB : dateB - dateA;
    });
    saveTasks();
  };

  loadTasks();

  return { tasks, isLoading, addTask, updateTask, deleteTask, sortTasksByDueDate };
});

2. 优化任务编辑组件

components/TaskEdit.vue

<template>
  <el-dialog v-model="visible" title="编辑任务" width="30%">
    <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
      <el-form-item label="标题" prop="title">
        <el-input v-model="form.title" />
      </el-form-item>
      <el-form-item label="描述">
        <el-input v-model="form.description" type="textarea" />
      </el-form-item>
      <el-form-item label="截止日期" prop="dueDate">
        <el-date-picker v-model="form.dueDate" type="date" format="YYYY-MM-DD" />
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="visible = false">取消</el-button>
      <el-button type="primary" @click="saveTask" :loading="saving">保存</el-button>
    </template>
  </el-dialog>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from 'vue';
import { useTaskStore } from '@/stores/task';
import { ElMessage, FormInstance } from 'element-plus';

export default defineComponent({
  props: {
    task: {
      type: Object,
      default: () => ({ id: 0, title: '', description: '', dueDate: '', status: 'todo' })
    }
  },
  emits: ['update:visible'],
  setup(props, { emit }) {
    const taskStore = useTaskStore();
    const visible = ref(false);
    const saving = ref(false);
    const form = ref({ ...props.task });
    const formRef = ref<FormInstance>();
    const rules = {
      title: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
      dueDate: [{ required: true, message: '请选择截止日期', trigger: 'change' }]
    };

    watch(() => props.task, (newTask) => {
      form.value = { ...newTask };
      visible.value = !!newTask.id;
    });

    const saveTask = async () => {
      if (!formRef.value) return;
      await formRef.value.validate(async (valid) => {
        if (valid) {
          saving.value = true;
          const success = taskStore.updateTask(props.task.id, {
            title: form.value.title,
            description: form.value.description,
            dueDate: new Date(form.value.dueDate).toISOString().split('T')[0]
          });
          saving.value = false;
          if (success) {
            ElMessage.success('任务已更新');
            visible.value = false;
          } else {
            ElMessage.error('更新失败,标题不能为空');
          }
        }
      });
    };

    watch(visible, (val) => emit('update:visible', val));

    return { visible, saving, form, formRef, rules, saveTask };
  }
});
</script>

3. 更新任务列表组件

components/TaskList.vue

<template>
  <div>
    <el-input
      v-model="newTaskTitle"
      placeholder="输入任务标题"
      @keyup.enter="addNewTask"
      style="margin-bottom: 20px;"
      :disabled="isLoading"
    />
    <el-table
      v-loading="isLoading"
      :data="filteredTasks"
      style="width: 100%;"
      row-key="id"
      height="400"
    >
      <el-table-column prop="title" label="任务标题" />
      <el-table-column prop="description" label="描述" />
      <el-table-column prop="dueDate" label="截止日期" sortable />
      <el-table-column label="状态" width="150">
        <template #default="{ row }">
          <el-select
            v-model="row.status"
            @change="updateTaskStatus(row)"
            :disabled="isLoading"
          >
            <el-option label="待办" value="todo" />
            <el-option label="进行中" value="in-progress" />
            <el-option label="已完成" value="done" />
          </el-select>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="150">
        <template #default="{ row }">
          <el-button type="primary" size="small" @click="editTask(row)" :disabled="isLoading">
            编辑
          </el-button>
          <el-button type="danger" size="small" @click="deleteTask(row.id)" :disabled="isLoading">
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="8">
        <el-select v-model="filterStatus" placeholder="筛选状态" :disabled="isLoading">
          <el-option label="全部" value="all" />
          <el-option label="待办" value="todo" />
          <el-option label="进行中" value="in-progress" />
          <el-option label="已完成" value="done" />
        </el-select>
      </el-col>
      <el-col :span="8">
        <el-button @click="sortTasks(true)" :disabled="isLoading">按截止日期升序</el-button>
        <el-button @click="sortTasks(false)" :disabled="isLoading">按截止日期降序</el-button>
      </el-col>
    </el-row>
    <TaskEdit :task="editingTask" v-model:visible="showEditDialog" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { useTaskStore } from '@/stores/task';
import { ElMessage } from 'element-plus';
import TaskEdit from './TaskEdit.vue';

export default defineComponent({
  components: { TaskEdit },
  setup() {
    const taskStore = useTaskStore();
    const newTaskTitle = ref('');
    const filterStatus = ref('all');
    const showEditDialog = ref(false);
    const editingTask = ref({ id: 0, title: '', description: '', dueDate: '', status: 'todo' });

    const isLoading = computed(() => taskStore.isLoading);

    const filteredTasks = computed(() => {
      if (filterStatus.value === 'all') return taskStore.tasks;
      return taskStore.tasks.filter(task => task.status === filterStatus.value);
    });

    const addNewTask = () => {
      if (newTaskTitle.value.trim()) {
        const success = taskStore.addTask({
          title: newTaskTitle.value,
          description: '',
          status: 'todo',
          dueDate: new Date().toISOString().split('T')[0]
        });
        if (success) {
          ElMessage.success('任务添加成功');
          newTaskTitle.value = '';
        } else {
          ElMessage.error('标题不能为空');
        }
      }
    };

    const updateTaskStatus = (task: { id: number; status: string }) => {
      taskStore.updateTask(task.id, { status: task.status });
      ElMessage.info('任务状态已更新');
    };

    const deleteTask = (id: number) => {
      taskStore.deleteTask(id);
      ElMessage.success('任务已删除');
    };

    const editTask = (task: typeof editingTask.value) => {
      editingTask.value = { ...task };
      showEditDialog.value = true;
    };

    const sortTasks = (ascending: boolean) => {
      taskStore.sortTasksByDueDate(ascending);
      ElMessage.info(`已按截止日期${ascending ? '升序' : '降序'}排序`);
    };

    return {
      newTaskTitle,
      filterStatus,
      filteredTasks,
      isLoading,
      showEditDialog,
      editingTask,
      addNewTask,
      updateTaskStatus,
      deleteTask,
      editTask,
      sortTasks
    };
  }
});
</script>

<style scoped>
.el-table {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.el-table th.el-table__cell {
  background-color: #f5f7fa;
}
.el-button {
  margin-right: 5px;
}
</style>

实现效果

  • 用户体验:
    • 输入验证:标题和日期必填,保存失败时显示错误提示。
    • 加载状态:数据加载时显示 v-loading,禁用交互。
    • 样式优化:表格添加阴影和圆角,表头背景调整。
  • 性能:
    • 响应式优化:仅追踪必要数据,减少渲染开销。
    • 虚拟滚动准备:表格高度固定,为后续优化奠定基础。

代码分析

Pinia Store

  • 加载状态:新增 isLoading 追踪数据加载。
  • 输入验证:addTask 和 updateTask 返回布尔值,确保标题非空。

TaskEdit 组件

  • 表单验证:使用 el-form 的 rules 校验标题和日期。
  • 保存反馈:添加 saving 状态,显示加载动画。

TaskList 组件

  • 加载反馈:通过 v-loading 和 :disabled 优化交互。
  • 样式美化:调整表格和按钮样式,提升视觉效果。
  • 性能准备:固定表格高度,便于集成虚拟滚动。

测试验证

手动测试

  1. 添加空标题任务,验证错误提示。
  2. 编辑任务,留空标题或日期,检查保存失败。
  3. 数据加载时操作,确认按钮禁用。

E2E 测试

cypress/e2e/taskList.cy.js

describe('TaskList UX and Performance', () => {
  beforeEach(() => cy.visit('/'));

  it('prevents adding empty task', () => {
    cy.get('input').type('{enter}');
    cy.get('.el-message--error').should('contain', '标题不能为空');
  });

  it('shows loading state', () => {
    cy.get('.el-table').should('have.class', 'el-loading-parent--relative');
    cy.wait(100); // 模拟加载时间
    cy.get('.el-table').should('not.have.class', 'el-loading-parent--relative');
  });

  it('edits task with validation', () => {
    cy.get('input').type('Test Task{enter}');
    cy.get('.el-table').contains('Test Task').parent().find('.el-button--primary').click();
    cy.get('.el-dialog').within(() => {
      cy.get('input').first().clear();
      cy.get('.el-button--primary').click();
      cy.get('.el-form-item__error').should('contain', '标题不能为空');
    });
  });
});

优化效果

  • 用户体验:
    • 更清晰的反馈机制。
    • 防止无效操作,提升交互流畅性。
  • 性能:
    • 减少不必要渲染,加载状态优化操作体验。
    • 为大规模数据渲染做好准备。

注意事项

  1. 虚拟滚动:
    • 当前未实现,若任务超过 1000 条,需引入 vue-virtual-scroller。
  2. 错误恢复:
    • LocalStorage 读取失败时可添加默认值。
  3. 样式冲突:
    • 确保全局 CSS 不影响 Element Plus 组件。

总结

本节通过输入验证、加载状态和样式优化,提升了 TaskMaster 的用户体验和性能基础。结合 Vue 3 的响应式特性和 Pinia 的状态管理,你学会了如何在实际项目中应用优化策略。本章结束,下一章将探讨更复杂的实战场景,整合你的 Vue 3 技能!

Last Updated:: 2/24/25, 6:40 PM