本章将介绍Vue.js的生态系统,并通过一个完整的实战项目来综合运用前面学到的知识。

10.1 Vue.js生态系统

UI组件库

Element Plus

# 安装Element Plus
npm install element-plus
npm install @element-plus/icons-vue
// src/main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'

const app = createApp(App)

// 注册Element Plus
app.use(ElementPlus)

// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

app.mount('#app')
<!-- src/components/UserForm.vue -->
<template>
  <el-form
    ref="formRef"
    :model="form"
    :rules="rules"
    label-width="120px"
    class="user-form"
  >
    <el-form-item label="用户名" prop="username">
      <el-input
        v-model="form.username"
        placeholder="请输入用户名"
        clearable
      />
    </el-form-item>
    
    <el-form-item label="邮箱" prop="email">
      <el-input
        v-model="form.email"
        type="email"
        placeholder="请输入邮箱"
        clearable
      />
    </el-form-item>
    
    <el-form-item label="角色" prop="role">
      <el-select
        v-model="form.role"
        placeholder="请选择角色"
        style="width: 100%"
      >
        <el-option
          v-for="role in roles"
          :key="role.value"
          :label="role.label"
          :value="role.value"
        />
      </el-select>
    </el-form-item>
    
    <el-form-item label="状态" prop="status">
      <el-switch
        v-model="form.status"
        active-text="启用"
        inactive-text="禁用"
      />
    </el-form-item>
    
    <el-form-item label="头像" prop="avatar">
      <el-upload
        class="avatar-uploader"
        action="/api/upload"
        :show-file-list="false"
        :on-success="handleAvatarSuccess"
        :before-upload="beforeAvatarUpload"
      >
        <img v-if="form.avatar" :src="form.avatar" class="avatar" />
        <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
      </el-upload>
    </el-form-item>
    
    <el-form-item>
      <el-button type="primary" @click="submitForm">提交</el-button>
      <el-button @click="resetForm">重置</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'

const formRef = ref()

const form = reactive({
  username: '',
  email: '',
  role: '',
  status: true,
  avatar: ''
})

const roles = [
  { label: '管理员', value: 'admin' },
  { label: '编辑者', value: 'editor' },
  { label: '用户', value: 'user' }
]

const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '请输入邮箱地址', trigger: 'blur' },
    { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
  ],
  role: [
    { required: true, message: '请选择角色', trigger: 'change' }
  ]
}

function handleAvatarSuccess(res) {
  form.avatar = res.data.url
}

function beforeAvatarUpload(file) {
  const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
  const isLt2M = file.size / 1024 / 1024 < 2

  if (!isJPG) {
    ElMessage.error('上传头像图片只能是 JPG/PNG 格式!')
  }
  if (!isLt2M) {
    ElMessage.error('上传头像图片大小不能超过 2MB!')
  }
  return isJPG && isLt2M
}

function submitForm() {
  formRef.value.validate((valid) => {
    if (valid) {
      console.log('提交表单:', form)
      ElMessage.success('提交成功!')
    } else {
      ElMessage.error('请检查表单输入!')
    }
  })
}

function resetForm() {
  formRef.value.resetFields()
}
</script>

<style scoped>
.user-form {
  max-width: 600px;
  margin: 0 auto;
}

.avatar-uploader .avatar {
  width: 178px;
  height: 178px;
  display: block;
}

.avatar-uploader .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
  border-color: var(--el-color-primary);
}

.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  text-align: center;
}
</style>

Vuetify

# 安装Vuetify
npm install vuetify@next @mdi/font
// src/plugins/vuetify.js
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { mdi } from 'vuetify/iconsets/mdi'
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'

export default createVuetify({
  components,
  directives,
  
  theme: {
    defaultTheme: 'light',
    themes: {
      light: {
        colors: {
          primary: '#1976D2',
          secondary: '#424242',
          accent: '#82B1FF',
          error: '#FF5252',
          info: '#2196F3',
          success: '#4CAF50',
          warning: '#FFC107'
        }
      },
      dark: {
        colors: {
          primary: '#2196F3',
          secondary: '#424242',
          accent: '#FF4081',
          error: '#FF5252',
          info: '#2196F3',
          success: '#4CAF50',
          warning: '#FB8C00'
        }
      }
    }
  },
  
  icons: {
    defaultSet: 'mdi',
    sets: {
      mdi
    }
  }
})

状态管理进阶

Pinia持久化插件

// src/stores/plugins/persist.js
export function createPersistPlugin(options = {}) {
  return (context) => {
    const { store } = context
    const {
      key = store.$id,
      storage = localStorage,
      paths = null,
      serializer = {
        serialize: JSON.stringify,
        deserialize: JSON.parse
      }
    } = options
    
    // 从存储中恢复状态
    function restore() {
      try {
        const stored = storage.getItem(key)
        if (stored) {
          const data = serializer.deserialize(stored)
          
          if (paths) {
            // 只恢复指定路径的状态
            paths.forEach(path => {
              if (data[path] !== undefined) {
                store.$patch({ [path]: data[path] })
              }
            })
          } else {
            // 恢复所有状态
            store.$patch(data)
          }
        }
      } catch (error) {
        console.error('Failed to restore state:', error)
      }
    }
    
    // 保存状态到存储
    function persist() {
      try {
        let data = store.$state
        
        if (paths) {
          // 只保存指定路径的状态
          data = {}
          paths.forEach(path => {
            if (store.$state[path] !== undefined) {
              data[path] = store.$state[path]
            }
          })
        }
        
        storage.setItem(key, serializer.serialize(data))
      } catch (error) {
        console.error('Failed to persist state:', error)
      }
    }
    
    // 初始化时恢复状态
    restore()
    
    // 监听状态变化并持久化
    store.$subscribe((mutation, state) => {
      persist()
    })
  }
}
// src/stores/user.js
import { defineStore } from 'pinia'
import { createPersistPlugin } from './plugins/persist'

export const useUserStore = defineStore('user', {
  state: () => ({
    profile: null,
    token: null,
    preferences: {
      theme: 'light',
      language: 'zh-CN',
      notifications: true
    },
    recentActivity: []
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.token,
    
    fullName: (state) => {
      if (!state.profile) return ''
      return `${state.profile.firstName} ${state.profile.lastName}`
    },
    
    avatar: (state) => {
      return state.profile?.avatar || '/default-avatar.png'
    }
  },
  
  actions: {
    async login(credentials) {
      try {
        const response = await fetch('/api/auth/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(credentials)
        })
        
        if (!response.ok) {
          throw new Error('Login failed')
        }
        
        const data = await response.json()
        
        this.token = data.token
        this.profile = data.user
        
        return data
      } catch (error) {
        console.error('Login error:', error)
        throw error
      }
    },
    
    logout() {
      this.token = null
      this.profile = null
      this.recentActivity = []
    },
    
    updatePreferences(newPreferences) {
      this.preferences = { ...this.preferences, ...newPreferences }
    },
    
    addActivity(activity) {
      this.recentActivity.unshift({
        ...activity,
        timestamp: new Date().toISOString()
      })
      
      // 只保留最近50条活动
      if (this.recentActivity.length > 50) {
        this.recentActivity = this.recentActivity.slice(0, 50)
      }
    }
  }
})

// 应用持久化插件
useUserStore().$persist = createPersistPlugin({
  key: 'user-store',
  paths: ['token', 'profile', 'preferences'] // 只持久化这些字段
})

工具库集成

VueUse

# 安装VueUse
npm install @vueuse/core
<!-- src/components/AdvancedFeatures.vue -->
<template>
  <div class="advanced-features">
    <!-- 暗黑模式切换 -->
    <div class="feature-section">
      <h3>暗黑模式</h3>
      <button @click="toggleDark()">
        {{ isDark ? '切换到亮色模式' : '切换到暗黑模式' }}
      </button>
    </div>
    
    <!-- 网络状态 -->
    <div class="feature-section">
      <h3>网络状态</h3>
      <p>在线状态: {{ isOnline ? '在线' : '离线' }}</p>
      <p>网络类型: {{ connection?.effectiveType || '未知' }}</p>
    </div>
    
    <!-- 电池状态 -->
    <div class="feature-section">
      <h3>电池状态</h3>
      <div v-if="battery">
        <p>电量: {{ Math.round(battery.level * 100) }}%</p>
        <p>充电状态: {{ battery.charging ? '充电中' : '未充电' }}</p>
        <p>剩余时间: {{ formatTime(battery.dischargingTime) }}</p>
      </div>
      <p v-else>不支持电池API</p>
    </div>
    
    <!-- 地理位置 -->
    <div class="feature-section">
      <h3>地理位置</h3>
      <button @click="getCurrentPosition">获取位置</button>
      <div v-if="coords">
        <p>纬度: {{ coords.latitude.toFixed(6) }}</p>
        <p>经度: {{ coords.longitude.toFixed(6) }}</p>
        <p>精度: {{ coords.accuracy }}米</p>
      </div>
      <p v-if="locatedAt">更新时间: {{ locatedAt.toLocaleString() }}</p>
    </div>
    
    <!-- 剪贴板 -->
    <div class="feature-section">
      <h3>剪贴板</h3>
      <input v-model="textToCopy" placeholder="输入要复制的文本" />
      <button @click="copy(textToCopy)">复制</button>
      <p v-if="copied">已复制!</p>
      <button @click="read()">读取剪贴板</button>
      <p v-if="text">剪贴板内容: {{ text }}</p>
    </div>
    
    <!-- 全屏 -->
    <div class="feature-section">
      <h3>全屏</h3>
      <button @click="toggle">
        {{ isFullscreen ? '退出全屏' : '进入全屏' }}
      </button>
    </div>
    
    <!-- 本地存储 -->
    <div class="feature-section">
      <h3>本地存储</h3>
      <input v-model="storageValue" placeholder="输入存储值" />
      <button @click="saveToStorage">保存</button>
      <p>存储的值: {{ storedValue || '无' }}</p>
    </div>
    
    <!-- 鼠标位置 -->
    <div class="feature-section">
      <h3>鼠标位置</h3>
      <p>X: {{ x }}, Y: {{ y }}</p>
    </div>
    
    <!-- 窗口大小 -->
    <div class="feature-section">
      <h3>窗口大小</h3>
      <p>宽度: {{ width }}px, 高度: {{ height }}px</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import {
  useDark,
  useToggle,
  useOnline,
  useNetwork,
  useBattery,
  useGeolocation,
  useClipboard,
  useFullscreen,
  useLocalStorage,
  useMouse,
  useWindowSize
} from '@vueuse/core'

// 暗黑模式
const isDark = useDark()
const toggleDark = useToggle(isDark)

// 网络状态
const isOnline = useOnline()
const connection = useNetwork()

// 电池状态
const { battery } = useBattery()

// 地理位置
const { coords, locatedAt, error, resume: getCurrentPosition } = useGeolocation()

// 剪贴板
const textToCopy = ref('')
const { text, copy, copied, read, isSupported: clipboardSupported } = useClipboard()

// 全屏
const { isFullscreen, toggle } = useFullscreen()

// 本地存储
const storageValue = ref('')
const storedValue = useLocalStorage('demo-value', '')

function saveToStorage() {
  storedValue.value = storageValue.value
}

// 鼠标位置
const { x, y } = useMouse()

// 窗口大小
const { width, height } = useWindowSize()

// 格式化时间
function formatTime(seconds) {
  if (!seconds || seconds === Infinity) return '未知'
  
  const hours = Math.floor(seconds / 3600)
  const minutes = Math.floor((seconds % 3600) / 60)
  
  if (hours > 0) {
    return `${hours}小时${minutes}分钟`
  }
  return `${minutes}分钟`
}
</script>

<style scoped>
.advanced-features {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

.feature-section {
  margin-bottom: 2rem;
  padding: 1rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
}

.feature-section h3 {
  margin-top: 0;
  color: #333;
}

.feature-section button {
  margin: 0.5rem 0.5rem 0.5rem 0;
  padding: 0.5rem 1rem;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.feature-section button:hover {
  background: #0056b3;
}

.feature-section input {
  margin: 0.5rem 0.5rem 0.5rem 0;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  width: 200px;
}

.feature-section p {
  margin: 0.5rem 0;
  color: #666;
}
</style>

10.2 实战项目:任务管理系统

项目结构

task-manager/
├── public/
│   ├── index.html
│   └── favicon.ico
├── src/
│   ├── api/
│   │   ├── index.js
│   │   ├── auth.js
│   │   ├── tasks.js
│   │   └── projects.js
│   ├── components/
│   │   ├── common/
│   │   │   ├── AppHeader.vue
│   │   │   ├── AppSidebar.vue
│   │   │   └── AppFooter.vue
│   │   ├── task/
│   │   │   ├── TaskList.vue
│   │   │   ├── TaskItem.vue
│   │   │   ├── TaskForm.vue
│   │   │   └── TaskFilter.vue
│   │   └── project/
│   │       ├── ProjectList.vue
│   │       ├── ProjectCard.vue
│   │       └── ProjectForm.vue
│   ├── composables/
│   │   ├── useAuth.js
│   │   ├── useTasks.js
│   │   ├── useProjects.js
│   │   └── useNotifications.js
│   ├── stores/
│   │   ├── auth.js
│   │   ├── tasks.js
│   │   ├── projects.js
│   │   └── ui.js
│   ├── router/
│   │   └── index.js
│   ├── views/
│   │   ├── Login.vue
│   │   ├── Dashboard.vue
│   │   ├── Tasks.vue
│   │   ├── Projects.vue
│   │   └── Profile.vue
│   ├── utils/
│   │   ├── request.js
│   │   ├── date.js
│   │   └── validation.js
│   ├── styles/
│   │   ├── main.css
│   │   ├── variables.css
│   │   └── components.css
│   ├── App.vue
│   └── main.js
├── package.json
└── vite.config.js

API层设计

// src/api/index.js
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'

// 创建axios实例
const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000
})

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    const authStore = useAuthStore()
    
    // 添加认证token
    if (authStore.token) {
      config.headers.Authorization = `Bearer ${authStore.token}`
    }
    
    // 添加请求ID用于追踪
    config.headers['X-Request-ID'] = generateRequestId()
    
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
request.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    const authStore = useAuthStore()
    
    // 处理认证错误
    if (error.response?.status === 401) {
      authStore.logout()
      ElMessage.error('登录已过期,请重新登录')
      return Promise.reject(error)
    }
    
    // 处理其他错误
    const message = error.response?.data?.message || error.message || '请求失败'
    ElMessage.error(message)
    
    return Promise.reject(error)
  }
)

// 生成请求ID
function generateRequestId() {
  return Math.random().toString(36).substring(2) + Date.now().toString(36)
}

export default request
// src/api/tasks.js
import request from './index'

export const taskApi = {
  // 获取任务列表
  getTasks(params = {}) {
    return request.get('/tasks', { params })
  },
  
  // 获取任务详情
  getTask(id) {
    return request.get(`/tasks/${id}`)
  },
  
  // 创建任务
  createTask(data) {
    return request.post('/tasks', data)
  },
  
  // 更新任务
  updateTask(id, data) {
    return request.put(`/tasks/${id}`, data)
  },
  
  // 删除任务
  deleteTask(id) {
    return request.delete(`/tasks/${id}`)
  },
  
  // 批量操作
  batchUpdate(ids, data) {
    return request.patch('/tasks/batch', { ids, data })
  },
  
  // 获取任务统计
  getTaskStats(projectId) {
    return request.get('/tasks/stats', {
      params: { projectId }
    })
  }
}

状态管理

// src/stores/tasks.js
import { defineStore } from 'pinia'
import { taskApi } from '@/api/tasks'

export const useTaskStore = defineStore('tasks', {
  state: () => ({
    tasks: [],
    currentTask: null,
    loading: false,
    filters: {
      status: '',
      priority: '',
      assignee: '',
      project: '',
      search: ''
    },
    pagination: {
      page: 1,
      size: 20,
      total: 0
    }
  }),
  
  getters: {
    filteredTasks: (state) => {
      let tasks = state.tasks
      
      // 状态过滤
      if (state.filters.status) {
        tasks = tasks.filter(task => task.status === state.filters.status)
      }
      
      // 优先级过滤
      if (state.filters.priority) {
        tasks = tasks.filter(task => task.priority === state.filters.priority)
      }
      
      // 负责人过滤
      if (state.filters.assignee) {
        tasks = tasks.filter(task => task.assigneeId === state.filters.assignee)
      }
      
      // 项目过滤
      if (state.filters.project) {
        tasks = tasks.filter(task => task.projectId === state.filters.project)
      }
      
      // 搜索过滤
      if (state.filters.search) {
        const search = state.filters.search.toLowerCase()
        tasks = tasks.filter(task => 
          task.title.toLowerCase().includes(search) ||
          task.description.toLowerCase().includes(search)
        )
      }
      
      return tasks
    },
    
    tasksByStatus: (state) => {
      const groups = {
        todo: [],
        inProgress: [],
        done: []
      }
      
      state.tasks.forEach(task => {
        if (groups[task.status]) {
          groups[task.status].push(task)
        }
      })
      
      return groups
    },
    
    taskStats: (state) => {
      const total = state.tasks.length
      const completed = state.tasks.filter(task => task.status === 'done').length
      const inProgress = state.tasks.filter(task => task.status === 'inProgress').length
      const todo = state.tasks.filter(task => task.status === 'todo').length
      
      return {
        total,
        completed,
        inProgress,
        todo,
        completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
      }
    }
  },
  
  actions: {
    async fetchTasks(params = {}) {
      this.loading = true
      
      try {
        const response = await taskApi.getTasks({
          ...params,
          page: this.pagination.page,
          size: this.pagination.size
        })
        
        this.tasks = response.data
        this.pagination.total = response.total
      } catch (error) {
        console.error('Failed to fetch tasks:', error)
        throw error
      } finally {
        this.loading = false
      }
    },
    
    async createTask(taskData) {
      try {
        const task = await taskApi.createTask(taskData)
        this.tasks.unshift(task)
        return task
      } catch (error) {
        console.error('Failed to create task:', error)
        throw error
      }
    },
    
    async updateTask(id, updates) {
      try {
        const updatedTask = await taskApi.updateTask(id, updates)
        
        const index = this.tasks.findIndex(task => task.id === id)
        if (index !== -1) {
          this.tasks[index] = updatedTask
        }
        
        if (this.currentTask?.id === id) {
          this.currentTask = updatedTask
        }
        
        return updatedTask
      } catch (error) {
        console.error('Failed to update task:', error)
        throw error
      }
    },
    
    async deleteTask(id) {
      try {
        await taskApi.deleteTask(id)
        
        const index = this.tasks.findIndex(task => task.id === id)
        if (index !== -1) {
          this.tasks.splice(index, 1)
        }
        
        if (this.currentTask?.id === id) {
          this.currentTask = null
        }
      } catch (error) {
        console.error('Failed to delete task:', error)
        throw error
      }
    },
    
    setFilters(filters) {
      this.filters = { ...this.filters, ...filters }
      this.pagination.page = 1 // 重置页码
    },
    
    clearFilters() {
      this.filters = {
        status: '',
        priority: '',
        assignee: '',
        project: '',
        search: ''
      }
      this.pagination.page = 1
    },
    
    setCurrentTask(task) {
      this.currentTask = task
    }
  }
})

组件实现

<!-- src/components/task/TaskList.vue -->
<template>
  <div class="task-list">
    <!-- 过滤器 -->
    <TaskFilter
      :filters="filters"
      @update:filters="updateFilters"
      @clear="clearFilters"
    />
    
    <!-- 统计信息 -->
    <div class="task-stats">
      <el-row :gutter="16">
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-number">{{ taskStats.total }}</div>
              <div class="stat-label">总任务</div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-number">{{ taskStats.completed }}</div>
              <div class="stat-label">已完成</div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-number">{{ taskStats.inProgress }}</div>
              <div class="stat-label">进行中</div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-number">{{ taskStats.completionRate }}%</div>
              <div class="stat-label">完成率</div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    
    <!-- 任务列表 -->
    <div class="task-content">
      <!-- 看板视图 -->
      <div v-if="viewMode === 'kanban'" class="kanban-view">
        <div class="kanban-column" v-for="(tasks, status) in tasksByStatus" :key="status">
          <div class="column-header">
            <h3>{{ getStatusLabel(status) }}</h3>
            <el-badge :value="tasks.length" class="item" />
          </div>
          
          <draggable
            v-model="tasksByStatus[status]"
            group="tasks"
            @change="handleTaskMove"
            class="task-column"
          >
            <template #item="{ element }">
              <TaskItem
                :task="element"
                @edit="editTask"
                @delete="deleteTask"
                @status-change="updateTaskStatus"
              />
            </template>
          </draggable>
        </div>
      </div>
      
      <!-- 列表视图 -->
      <div v-else class="list-view">
        <el-table
          :data="filteredTasks"
          v-loading="loading"
          @selection-change="handleSelectionChange"
        >
          <el-table-column type="selection" width="55" />
          
          <el-table-column prop="title" label="标题" min-width="200">
            <template #default="{ row }">
              <div class="task-title">
                <el-link @click="viewTask(row)">{{ row.title }}</el-link>
                <div class="task-tags">
                  <el-tag
                    v-for="tag in row.tags"
                    :key="tag"
                    size="small"
                    class="tag"
                  >
                    {{ tag }}
                  </el-tag>
                </div>
              </div>
            </template>
          </el-table-column>
          
          <el-table-column prop="status" label="状态" width="120">
            <template #default="{ row }">
              <el-select
                :model-value="row.status"
                @change="updateTaskStatus(row, $event)"
                size="small"
              >
                <el-option label="待办" value="todo" />
                <el-option label="进行中" value="inProgress" />
                <el-option label="已完成" value="done" />
              </el-select>
            </template>
          </el-table-column>
          
          <el-table-column prop="priority" label="优先级" width="100">
            <template #default="{ row }">
              <el-tag
                :type="getPriorityType(row.priority)"
                size="small"
              >
                {{ getPriorityLabel(row.priority) }}
              </el-tag>
            </template>
          </el-table-column>
          
          <el-table-column prop="assignee" label="负责人" width="120">
            <template #default="{ row }">
              <div class="assignee">
                <el-avatar :size="24" :src="row.assignee?.avatar" />
                <span>{{ row.assignee?.name }}</span>
              </div>
            </template>
          </el-table-column>
          
          <el-table-column prop="dueDate" label="截止日期" width="120">
            <template #default="{ row }">
              <span :class="{ 'overdue': isOverdue(row.dueDate) }">
                {{ formatDate(row.dueDate) }}
              </span>
            </template>
          </el-table-column>
          
          <el-table-column label="操作" width="150">
            <template #default="{ row }">
              <el-button size="small" @click="editTask(row)">编辑</el-button>
              <el-button
                size="small"
                type="danger"
                @click="deleteTask(row)"
              >
                删除
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        
        <!-- 分页 -->
        <el-pagination
          v-model:current-page="pagination.page"
          v-model:page-size="pagination.size"
          :total="pagination.total"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </div>
    
    <!-- 批量操作 -->
    <div v-if="selectedTasks.length > 0" class="batch-actions">
      <el-button @click="batchUpdateStatus('done')">标记为完成</el-button>
      <el-button @click="batchDelete" type="danger">批量删除</el-button>
    </div>
    
    <!-- 任务表单对话框 -->
    <el-dialog
      v-model="showTaskForm"
      :title="editingTask ? '编辑任务' : '创建任务'"
      width="600px"
    >
      <TaskForm
        :task="editingTask"
        @submit="handleTaskSubmit"
        @cancel="closeTaskForm"
      />
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { ElMessage, ElMessageBox } from 'element-plus'
import draggable from 'vuedraggable'
import { useTaskStore } from '@/stores/tasks'
import TaskFilter from './TaskFilter.vue'
import TaskItem from './TaskItem.vue'
import TaskForm from './TaskForm.vue'
import { formatDate } from '@/utils/date'

const taskStore = useTaskStore()
const {
  tasks,
  filteredTasks,
  tasksByStatus,
  taskStats,
  loading,
  filters,
  pagination
} = storeToRefs(taskStore)

const viewMode = ref('list') // 'list' | 'kanban'
const selectedTasks = ref([])
const showTaskForm = ref(false)
const editingTask = ref(null)

// 状态标签映射
const statusLabels = {
  todo: '待办',
  inProgress: '进行中',
  done: '已完成'
}

// 优先级标签映射
const priorityLabels = {
  low: '低',
  medium: '中',
  high: '高',
  urgent: '紧急'
}

// 优先级类型映射
const priorityTypes = {
  low: '',
  medium: 'warning',
  high: 'danger',
  urgent: 'danger'
}

function getStatusLabel(status) {
  return statusLabels[status] || status
}

function getPriorityLabel(priority) {
  return priorityLabels[priority] || priority
}

function getPriorityType(priority) {
  return priorityTypes[priority] || ''
}

function isOverdue(dueDate) {
  if (!dueDate) return false
  return new Date(dueDate) < new Date()
}

function updateFilters(newFilters) {
  taskStore.setFilters(newFilters)
  fetchTasks()
}

function clearFilters() {
  taskStore.clearFilters()
  fetchTasks()
}

function handleSelectionChange(selection) {
  selectedTasks.value = selection
}

function handleSizeChange(size) {
  pagination.value.size = size
  fetchTasks()
}

function handleCurrentChange(page) {
  pagination.value.page = page
  fetchTasks()
}

function handleTaskMove(event) {
  if (event.moved) {
    const { element, newIndex } = event.moved
    // 根据新位置确定状态
    const newStatus = Object.keys(tasksByStatus.value).find(status => 
      tasksByStatus.value[status].includes(element)
    )
    
    if (newStatus && element.status !== newStatus) {
      updateTaskStatus(element, newStatus)
    }
  }
}

async function updateTaskStatus(task, status) {
  try {
    await taskStore.updateTask(task.id, { status })
    ElMessage.success('任务状态更新成功')
  } catch (error) {
    ElMessage.error('更新失败')
  }
}

function editTask(task) {
  editingTask.value = task
  showTaskForm.value = true
}

function createTask() {
  editingTask.value = null
  showTaskForm.value = true
}

function viewTask(task) {
  // 跳转到任务详情页
  console.log('View task:', task)
}

async function deleteTask(task) {
  try {
    await ElMessageBox.confirm(
      `确定要删除任务 "${task.title}" 吗?`,
      '确认删除',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    
    await taskStore.deleteTask(task.id)
    ElMessage.success('删除成功')
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('删除失败')
    }
  }
}

async function batchUpdateStatus(status) {
  try {
    const ids = selectedTasks.value.map(task => task.id)
    await taskStore.batchUpdate(ids, { status })
    ElMessage.success('批量更新成功')
    selectedTasks.value = []
    fetchTasks()
  } catch (error) {
    ElMessage.error('批量更新失败')
  }
}

async function batchDelete() {
  try {
    await ElMessageBox.confirm(
      `确定要删除选中的 ${selectedTasks.value.length} 个任务吗?`,
      '确认删除',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    
    const deletePromises = selectedTasks.value.map(task => 
      taskStore.deleteTask(task.id)
    )
    
    await Promise.all(deletePromises)
    ElMessage.success('批量删除成功')
    selectedTasks.value = []
    fetchTasks()
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('批量删除失败')
    }
  }
}

async function handleTaskSubmit(taskData) {
  try {
    if (editingTask.value) {
      await taskStore.updateTask(editingTask.value.id, taskData)
      ElMessage.success('任务更新成功')
    } else {
      await taskStore.createTask(taskData)
      ElMessage.success('任务创建成功')
    }
    
    closeTaskForm()
    fetchTasks()
  } catch (error) {
    ElMessage.error('操作失败')
  }
}

function closeTaskForm() {
  showTaskForm.value = false
  editingTask.value = null
}

async function fetchTasks() {
  try {
    await taskStore.fetchTasks()
  } catch (error) {
    ElMessage.error('获取任务列表失败')
  }
}

onMounted(() => {
  fetchTasks()
})

defineExpose({
  createTask
})
</script>

<style scoped>
.task-list {
  padding: 1rem;
}

.task-stats {
  margin: 1rem 0;
}

.stat-card {
  text-align: center;
}

.stat-content {
  padding: 1rem;
}

.stat-number {
  font-size: 2rem;
  font-weight: bold;
  color: #409eff;
}

.stat-label {
  color: #666;
  margin-top: 0.5rem;
}

.kanban-view {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  padding: 1rem 0;
}

.kanban-column {
  flex: 1;
  min-width: 300px;
  background: #f5f5f5;
  border-radius: 8px;
  padding: 1rem;
}

.column-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1rem;
}

.column-header h3 {
  margin: 0;
  color: #333;
}

.task-column {
  min-height: 400px;
}

.task-title {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.task-tags {
  display: flex;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.tag {
  font-size: 0.75rem;
}

.assignee {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.overdue {
  color: #f56c6c;
  font-weight: bold;
}

.batch-actions {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  background: white;
  padding: 1rem;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  border: 1px solid #e0e0e0;
}
</style>

本章小结

本章我们学习了Vue.js的生态系统和实战项目开发:

  1. UI组件库:Element Plus、Vuetify等主流组件库的使用
  2. 工具库集成:VueUse等实用工具库的应用
  3. 状态管理进阶:Pinia插件开发和持久化
  4. 实战项目:任务管理系统的完整实现

下一章预告

下一章我们将学习Vue.js的最佳实践和进阶技巧,包括: - 代码规范和团队协作 - 性能优化进阶技巧 - 微前端架构 - Vue.js 3.x新特性深入

练习题

基础练习

  1. 组件库使用

    • 使用Element Plus创建一个完整的表单
    • 实现数据表格的增删改查
    • 添加图表展示功能
  2. 状态管理

    • 实现Pinia状态持久化
    • 创建模块化的Store结构
    • 添加状态变更日志

进阶练习

  1. 实战项目

    • 完善任务管理系统的功能
    • 添加实时通知功能
    • 实现数据导入导出
  2. 生态系统集成

    • 集成多个UI组件库
    • 创建自定义插件
    • 实现主题切换功能

提示:实战项目是学习Vue.js最好的方式,通过完整的项目开发可以更好地理解和掌握各种技术点。