本章将介绍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的生态系统和实战项目开发:
- UI组件库:Element Plus、Vuetify等主流组件库的使用
- 工具库集成:VueUse等实用工具库的应用
- 状态管理进阶:Pinia插件开发和持久化
- 实战项目:任务管理系统的完整实现
下一章预告
下一章我们将学习Vue.js的最佳实践和进阶技巧,包括: - 代码规范和团队协作 - 性能优化进阶技巧 - 微前端架构 - Vue.js 3.x新特性深入
练习题
基础练习
组件库使用:
- 使用Element Plus创建一个完整的表单
- 实现数据表格的增删改查
- 添加图表展示功能
状态管理:
- 实现Pinia状态持久化
- 创建模块化的Store结构
- 添加状态变更日志
进阶练习
实战项目:
- 完善任务管理系统的功能
- 添加实时通知功能
- 实现数据导入导出
生态系统集成:
- 集成多个UI组件库
- 创建自定义插件
- 实现主题切换功能
提示:实战项目是学习Vue.js最好的方式,通过完整的项目开发可以更好地理解和掌握各种技术点。