|
我们将探讨如何使用Vue.js从零开始创建一个类似于Trello的任务管理应用程序。如果你不熟悉Trello,它是一款非常流行的任务管理工具,允许你把任务写在卡片上,然后通过一个看板的方式来直观地管理这些任务。Trello不仅可以用于个人的任务管理,还可以作为团队协作工具,在许多公司和组织中得到了广泛的应用。我们的目标是创建一个具有Trello核心功能的应用程序,包括创建任务卡片,以及通过拖放操作来移动这些卡片,以此来改变任务的状态或优先级。
在这里我们将使用Vue.js来实现一个简单的Trello功能。这篇文章将分为两部分。在第一部分,我们将探讨如何使用Vue.js的Draggable属性和拖动事件来实现任务的移动和类别(列)的创建。我们将详细介绍如何设置这些功能,并提供代码示例来帮助你理解。在第二部分,我们将深入讨论如何使用TypeScript和Vue的Composition API来描述我们的应用程序。可以更好的了解如何由多个组件(例如 props 和 emit)组成的应用程序中设置 TypeScript。
我们将使用前面介绍的TypeScript知识点来完成Trello功能的实现,系统这些代码将为你理解TypeScript提供有价值的信息和实践经验。我们将尽可能清晰地解释每一步,以便你能够跟上并理解我们正在做什么。阅读完这篇文章后,你将能够创建一个功能齐全的任务管理应用程序,并有足够的知识去定制和扩展它以满足你自己的需求。如果你是一个初学者,或者你想要了解如何使用拖动进行移动处理,那么这篇文章将会是一个很好的起点。
第一章 Vue3项目创建 1 Vue CLI 创建vue项目
第一章 Vue3项目创建 2 使用 Webpack 5 搭建 vue项目
第一章 Vue3项目创建 3 Vite 创建 vue项目
第二章 Vue3 基础语法指令
第三章 Vue Router路由器的使用
第四章 VUE常用 UI 库 1 ( element-plus,Ant ,naiveui,ArcoDesign)
第四章 VUE常用 UI 库 2 ( ailwind 后台框架)
第五章 Vue 组件应用 1( Props )
第五章 Vue 组件应用 2 ( Emit )
第五章 Vue 组件应用 3( Slots )
第五章 Vue 组件应用 4 ( provide 和 inject )
第五章 Vue 组件应用 5 (Vue 插件)
第六章 Pinia,Vuex与axios,VueUse 1(Pinia)
第六章 Pinia,Vuex与axios,VueUse 2(Vuex)
第六章 Pinia,Vuex与axios,VueUse 3(VueUse)
第六章 Pinia,Vuex与axios,VueUse 4(axios)
第七章 TypeScript 上
第七章 TypeScript 中
第七章 TypeScript 下 创建Trello 任务管理器
1 导入UI样式
npm init vite@latest zht-trello
cd zht-trello
npm install
npm run dev
npm install --save-dev @arco-design/web-vue
我们使用字节的UIarco-design来作为这个例子的主体架构。
2 创建任务数据
在src文件夹下创建文件夹data,在data中创建两个文件,创建class.json和tasks.json文件。
class.json
[
{
"id": 1,
"name": "看板任务一",
"collapsed": false
},
{
"id": 2,
"name": "看板任务二",
"collapsed": false
},
{
"id": 3,
"name": "看板任务二",
"collapsed": false
}
]
tasks.json
[
{
"id": 1,
"category_id": 1,
"name": "任务1",
"start_date": "2022-12-18",
"end_date": "2022-12-20",
"incharge_user": "张童",
"percentage": 100
},
{
"id": 2,
"category_id": 1,
"name": "任务1",
"start_date": "2020-12-19",
"end_date": "2020-12-23",
"incharge_user": "王鑫",
"percentage": 90
},
{
"id": 3,
"category_id": 3,
"name": "任务3",
"start_date": "2022-12-19",
"end_date": "2022-12-21",
"incharge_user": "王鑫",
"percentage": 40
},
{
"id": 4,
"category_id": 2,
"name": "任务4",
"start_date": "2022-12-21",
"end_date": "2022-12-30",
"incharge_user": "李佳",
"percentage": 60
},
{
"id": 5,
"category_id": 2,
"name": "任务5",
"start_date": "2022-12-20",
"end_date": "2022-12-22",
"incharge_user": "王芳",
"percentage": 5
},
{
"id": 6,
"category_id": 1,
"name": "任务6",
"start_date": "2022-12-28",
"end_date": "2022-12-08",
"incharge_user": "王芳",
"percentage": 0
}
]
使用 ref 函数将class.json与tasks.json保存成反应数据,方便在后边的代码中使用。
<script setup lang="ts">
import class_data from "./data/class.json";
import task_data from "./data/tasks.json";
import { ref } from "vue";
const classes = ref(class_data);
const tasks = ref(task_data);
</script>
<template>
</template>
<style scoped>
</style>
3 ref 函数的类型设置
现在我们使用 TypeScript 为 ref
函数来设置类型。尽管代码会自动执行 TypeScript 的类型推断功能,即使没有显式设置类型,也不会显示错误消息,但是我们仍然建议尽可能地显式声明类型,以提高代码的可读性和可维护性。
首先,我们需要创建一个用于管理应用程序中使用的类型的文件夹。在 src
文件夹中创建一个名为 types
的新文件夹。由于我们要创建的应用程序规模较小,因此我们将所有类型都存储在一个名为 index.ts
的文件中,并将它们导出以供我们的组件使用。
应用程序中有两种主要的类型:类别 (CateClass
) 和任务 (Task
)。这些类型是基于固定 JSON 文件数据定义的。在类型定义中我们也可以使用 Type 而不是 Interface。
export interface CateClass {
id: number;
name: string;
collapsed?: boolean;
}
export interface Task {
id: number;
category_id: number;
name: string;
start_date: string;
end_date: string;
incharge_user: string;
percentage: number;
}
请注意,标有 ?
的属性是可选的,因此即使缺少该属性也不会发生错误。
现在我们已经定义了类型,接下来我们需要在 App.vue
文件中导入这些类型,并使用 ref
函数来设置它们。这里是如何做到这一点的:
<script setup lang="ts">
import type {CateClass, Task } from "./data/tasksIndex";
import class_data from "./data/class.json";
import task_data from "./data/tasks.json";
import { ref } from "vue";
// 使用 ref 函数和我们定义的类型来设置 classes 和 tasks 的类型
const classes = ref<CateClass[]>(class_data);
const tasks = ref<Task[]>(task_data);
</script>
<template>
</template>
<style scoped>
</style>
在上面的代码中,ref
函数用于创建一个响应式的引用,其值可以被改变。我们使用 <CateClass[]>
和 <Task[]>
来显式地指定 classes
和 tasks
的类型,这样就可以确保它们总是包含正确类型的数据。
4 设置计算属性
创建了一个名为 renderCategoryTask
的计算属性函数。该计算属性通过遍历 classes
数组,并根据每个类别的 id
属性筛选出对应的任务,生成一个新的数组。这样,我们就得到了一个包含类别和对应任务的嵌套数据结构。就可以在模板中使用这个计算属性来渲染类别和任务组件。
在模板中我们使用 v-for
指令遍历 renderCategoryTask
数组,并将每个类别渲染为一个 a-col
组件。在 a-card
组件中,我们显示了类别的名称,并使用 v-if
条件指令来判断是否有任务,如果有则渲染任务列表。
<script setup lang="ts">
import type {CateClass, Task,CateClassTask } from "./data/tasksIndex";
import class_data from "./data/class.json";
import task_data from "./data/tasks.json";
import { ref, computed } from "vue";
const classes = ref<CateClass[]>(class_data);
const tasks = ref<Task[]>(task_data);
const renderCategoryTask = (() => {
return classes.value.map((classes) => {
const filterTasks = tasks.value.filter(
(task) => task.category_id === classes.id
);
return {
id: classes.id,
name: classes.name,
tasks: filterTasks,
};
});
});
</script>
<template>
<div
:style="{
boxSizing: 'border-box',
width: '100%',
padding: '40px',
backgroundColor: 'var(--color-fill-2)',
}"
>
<h1 >Trello 任务管理</h1>
<a-row :gutter="20" :style="{ marginBottom: '20px' }">
<a-col :span="8" v-for="category in renderCategoryTask" :key="category.id">
<a-card :title="category.name"
:bordered="false"
:style="{ width: '100%',height:'300px'}">
<template #extra>
<a-link>详细</a-link>
</template>
<a-list v-for="task in category.tasks" :key="task.id">
<a-list-item>{{task.name}} || {{task.incharge_user}}</a-list-item>
</a-list>
</a-card>
</a-col>
</a-row>
</div>
</template>
<style scoped>
</style>
5 组件设置
现在我们要将类别和任务的描述组件化。通过将其做成一个组件,大家可以加深对使用 TypeScript 时,通过 emit 来设置 props 和事件设置的理解。
在 CategoryItem.vue 文件中,我们使用 defineProps 来设置从 props 传递过来的 categoryTask。defineProps 不需要执行 import。defineProps 指定使用泛型传递的 props 类型。这里的 CategoryTask 是类型。
<template>
<a-col :span="8">
<a-card :title="category?.name" :bordered="false" :style="{ width: '300px' }">
<template #extra>
<a-link>详细</a-link>
</template>
<a-list v-for="task in category.tasks" :key="task.id">
<TaskItem :task="task" draggable="true"></TaskItem>
</a-list>
</a-card>
</a-col>
</template>
CategoryItem.vue
首先,在组件文件夹中创建一个 CategoryItem.vue 文件。起初,只有 script 和 template 标签没有描述任何处理。
<script setup lang="ts">
import type { CateClassTask } from "../data/tasksIndex";
import TaskItem from "./TaskItem.vue";
interface Props {
category: CateClassTask;
}
defineProps<Props>();
</script>
<template>
<a-col :span="8" >
<a-card :title="category?.name"
:bordered="false"
:style="{ width: '100%',height:'300px'}">
<template #extra>
<a-link>详细</a-link>
</template>
<a-list v-for="task in category.tasks" :key="task.id" >
<TaskItem :task="task" draggable="true"/>
</a-list>
</a-card>
</a-col>
</template>
此外,你还可以创建 TaskItem 组件来显示任务。对于 TaskItem 组件,描述如下,这样你就可以在 props 中接收任务。
TaskItem.vue
<script setup lang="ts">
import type { Task } from "../data/tasksIndex";
defineProps<{
task: Task;
}>();
</script>
<template>
<a-list-item>{{task.name}} || {{task.incharge_user}}</a-list-item>
</template>
在 CategoryItem.vue 文件中导入创建的 TaskItem 组件。在模板标签中使用导入的 TaskItem 组件。对于 props,在 TaskItem.vue 文件中的 props 任务集中传递使用 v-for 指令展开的任务。
6 可拖动设置
我们将实现同一类别(列)内的任务移动,我们需要将任务元素设置为可拖动。首先在CategoryItem.vue
代码中,添加 draggable
属性并将其设置为 true
以使元素可拖动。设置一个setDragTask拖动事件,通过这个单击事件来抓取拖动组件中的元素,并在按下鼠标按钮的同时移动元素。
<TaskItem :task="task"
draggable="true" //设置为可以移动
@dragstart="setDragTask"
/>
<script setup lang="ts">
//设置移动事件
const setDragTask = () => {
console.log('drag');
};
</script>
在dragstart事件中设置setDragTask方法并检查操作。v-on指令可用于配置事件,这里使用缩写@。当在浏览器中移动组件的时候,在开发者工具控制台中会显示字符串“drag”。
7 移动被拖动元素
1 在CategoryItem.vue组件中设置dragover事件。在dragover事件中调用dragOverTask函数,并将task作为参数传递进去。
<CategoryItem
class="min-w-[400px]"
v-for="categoryTask in renderCategoryTask"
:key="categoryTask.id"
:categoryTask="categoryTask"
@setDragTask="setDragTask"
@dragOverTask="dragOverTask"
/>
2 CategoryItem.vue脚本模块里在emit中,将事件名称设置为dragOverTask,并像之前的dragstart事件一样使用emit将任务传递给父组件。
const emit = defineEmits<{
(e: "setDragTask", task: Task): void;
(e: "dragOverTask", task: Task): void;
}>();
const setDragTask = (task: Task) => {
emit("setDragTask", task);
};
const dragOverTask = (task: Task) => {
emit("dragOverTask", task);
};
3 在dragOverTask函数中,检查传递过来的overTask信息以及之前设置的dragTask内容,以进行操作
const dragOverTask = (overTask: Task) => {
console.log('task:', dragTask.value);
console.log('overTask:', overTask);
};
4 在检查操作时,只有当dragTask的id和overTask的id不同时才执行移动操作。注意,我们在dragTask.value后面添加了?,因为dragTask的值可能为null,如果不添加?,将会显示错误消息。
const dragOverTask = (overTask: Task) => {
if (dragTask.value?.id !== overTask.id) {
const deleteIndex = tasks.value.findIndex(
(task) => task.id === dragTask.value?.id
);
const addIndex = tasks.value.findIndex((task) => task.id === overTask.id);
if (dragTask.value !== null) {
tasks.value.splice(deleteIndex, 1);
tasks.value.splice(addIndex, 0, dragTask.value);
}
}
};
8 trello 代码
trello项目结构
zht-trello
|---node_modules
|---public
| |--index.html // 项目启动页面
|---src // 代码源文件
| |--assets // 资源目录
| |--components // 组件目录
| | |-- CategoryItem.vue // 看板程序
| | |-- TaskItem.vue // 任务条
| | |-- data
| | | |-- class.json //
| | | |-- tasks.json
| | | |-- tasksIndex.ts
| |--main.js // 入口文件
| |--App.vue // 主程序
|----package.json
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import ArcoVue from '@arco-design/web-vue';
import '@arco-design/web-vue/dist/arco.css';
const app = createApp(App)
app.use(ArcoVue);
app.mount('#app')
App.vue
<script setup lang="ts">
import CategoryItem from "./components/CategoryItem.vue";
import type {CateClass, Task,CateClassTask } from "./data/tasksIndex";
import class_data from "./data/class.json";
import task_data from "./data/tasks.json";
import { ref, computed } from "vue";
const classes = ref<CateClass[]>(class_data);
const tasks = ref<Task[]>(task_data);
const dragTask = ref<Task | null>(null);
const renderCategoryTask = computed(() => {
return classes.value.map((classes) => {
const filterTasks:Task[] = tasks.value.filter(
(task) => task.category_id === classes.id
);
return {
id: classes.id,
name: classes.name,
tasks: filterTasks,
collapsed:classes.collapsed
} as CateClassTask;
})
});
const setDragTask = (task: Task) => {
dragTask.value = task;
};
const dragOverTask = (overTask: Task) => {
if (dragTask.value?.id !== overTask.id) {
const deleteIndex = tasks.value.findIndex(
(task) => task.id === dragTask.value?.id
);
const addIndex = tasks.value.findIndex((task) => task.id === overTask.id);
if (dragTask.value !== null) {
tasks.value.splice(deleteIndex, 1);
dragTask.value.category_id = overTask.category_id; //追加
tasks.value.splice(addIndex, 0, dragTask.value);
}
}
};
const dragOverCategory = (categoryTask: CateClassTask) => {
if (dragTask.value?.category_id !== categoryTask.id) {
const filterTasks = tasks.value.filter(
(task) => task.category_id === categoryTask.id
);
if (filterTasks.length === 0 && dragTask.value !== null)
dragTask.value.category_id = categoryTask.id;
}
};
</script>
<template>
<div
:style="{
boxSizing: 'border-box',
width: '100%',
padding: '40px',
backgroundColor: 'var(--color-fill-2)',
}"
>
<h1 >Trello 任务管理</h1>
<a-row :gutter="20" :style="{ marginBottom: '20px' }">
<CategoryItem
v-for="category in renderCategoryTask"
:key="category.id"
:category="category"
@dragover="dragOverCategory(category)"
@setDragTask="setDragTask"
@dragOverTask="dragOverTask"
/>
</a-row>
</div>
</template>
<style scoped>
</style>
CategoryItem.vue
<script setup lang="ts">
import type { Task,CateClassTask } from "../data/tasksIndex";
import TaskItem from "./TaskItem.vue";
interface Props {
category: CateClassTask;
}
defineProps<Props>();
const emit = defineEmits<{
(e: "setDragTask", task: Task): void;
(e: "dragOverTask", task: Task): void;
}>();
const setDragTask = (task: Task) => {
emit("setDragTask", task);
};
const dragOverTask = (task: Task) => {
emit("dragOverTask", task);
};
</script>
<template>
<a-col :span="8" >
<a-card :title="category?.name"
:bordered="false"
:style="{ width: '100%',height:'300px'}"
>
<template #extra>
<a-link>详细</a-link>
</template>
<a-list v-for="task in category.tasks" :key="task.id" >
<TaskItem :task="task"
draggable="true"
@dragstart="setDragTask(task)"
@dragover="dragOverTask(task)"
/>
</a-list>
</a-card>
</a-col>
</template>
TaskItem.vue
<script setup lang="ts">
import type { Task } from "../data/tasksIndex";
defineProps<{
task: Task;
}>();
</script>
<template>
<a-list-item>{{task.name}} || {{task.incharge_user}}</a-list-item>
<emplate>
tasksIndex.ts
export interface CateClass {
id: number;
name: string;
collapsed?: boolean;
}
export interface Task {
id: number;
category_id: number;
name: string;
start_date: string;
end_date: string;
incharge_user: string;
percentage: number;
}
export interface CateClassTask {
id: number;
name: string;
collapsed?: boolean;
tasks: Task[];
}