外观
vue3
约 1354 字大约 5 分钟
预计使用技术栈:
- Vue3
- TypeScript
- Vite
- Pinia
- Unocss
- Element Plus
后端:
- NestJS
- Prisma
- MySQL
- Redis
四大 ORM 框架
- TypeORM
- Prisma 2024 推荐
- Sequelize
- MikroORM
功能介绍
- 任务的增删改查 (CRUD)
- 任务状态切换(完成/未完成)
- 任务优先级设置
- 搜索和筛选功能
- 后端 API 的集成与同步
- Redis 缓存(后端)
- 持久化(MySQL 数据库)
项目结构
/todo-list
│
├── /frontend // 前端代码
│ ├── /src
│ │ ├── /components
│ │ ├── /store
│ │ ├── /views
│ │ │ ├── ToDoList.vue
│ │ ├── /router
│ │ ├── App.vue
│ │ └── main.ts
│ └── vite.config.ts
│
└── /backend // 后端代码
├── /src
│ ├── /modules
│ │ ├── todo
│ │ │ ├── todo.controller.ts
│ │ │ └── todo.service.ts
│ ├── app.module.ts
│ ├── main.ts
│ └── prisma.service.ts
└── prisma/schema.prisma前端详细实现
node 版本:>=18.17.1
1. 初始化项目并安装依赖
npm create vite@latest frontend
cd frontend
npm install
npm install vue-router@4 pinia element-plus axios unocss2. 创建 Pinia Store
// src/store/todoStore.ts
import { defineStore } from "pinia";
import axios from "axios";
export interface Todo {
id: number;
title: string;
priority: "Low" | "Medium" | "High";
completed: boolean;
}
const baseUrl = "http://localhost:3000";
export const useTodoStore = defineStore("todo", {
state: () => ({
todos: [] as Todo[],
filter: "" as string,
}),
actions: {
async fetchTodos() {
const { data } = await axios.get(`${baseUrl}/todo`);
this.todos = data;
},
async addTodo(title: string, priority: "Low" | "Medium" | "High") {
const { data } = await axios.post(`${baseUrl}/todo`, { title, priority });
this.todos.push(data);
},
async updateTodo(id: number, updatedFields: Partial<Todo>) {
await axios.put(`${baseUrl}/todo/${id}`, updatedFields);
this.fetchTodos();
},
async deleteTodo(id: number) {
await axios.delete(`${baseUrl}/todo/${id}`);
this.todos = this.todos.filter((todo) => todo.id !== id);
},
setFilter(filter: string) {
this.filter = filter;
},
},
getters: {
filteredTodos: (state) => {
if (state.filter === "completed") {
return state.todos.filter((todo) => todo.completed);
} else if (state.filter === "pending") {
return state.todos.filter((todo) => !todo.completed);
}
return state.todos;
},
},
});3. 创建 Vue Router
// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import Home from "./../components/HelloWorld.vue";
import ToDoList from "../views/ToDoList.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", component: Home },
{ path: "/todo", component: ToDoList },
],
});
export default router;4. 创建 App.vue
<script setup lang="ts"></script>
<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<router-view />
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>5. 创建 main.ts
// src/main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import router from "./router";
import "./style.css";
import App from "./App.vue";
const pinia = createPinia();
createApp(App).use(pinia).use(ElementPlus).use(router).mount("#app");创建 ToDoList.vue
<template>
<el-container>
<el-header>TODO List</el-header>
<el-main>
<el-input
v-model="newTask"
placeholder="Add a new task"
@keyup.enter="addNewTodo"
/>
<el-select v-model="priority" placeholder="Select Priority">
<el-option label="Low" value="Low"></el-option>
<el-option label="Medium" value="Medium"></el-option>
<el-option label="High" value="High"></el-option>
</el-select>
<el-button @click="addNewTodo">添加</el-button>
<el-table :data="filteredTodos" style="width: 100%">
<el-table-column prop="title" label="Task"></el-table-column>
<el-table-column prop="priority" label="Priority"></el-table-column>
<el-table-column label="Completed">
<template #default="scope">
<el-checkbox
v-model="scope.row.completed"
@change="toggleCompletion(scope.row.id)"
></el-checkbox>
</template>
</el-table-column>
<el-table-column>
<template #default="scope">
<el-button type="danger" @click="deleteTodo(scope.row.id)"
>Delete</el-button
>
</template>
</el-table-column>
</el-table>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref, onBeforeMount, computed } from "vue";
import { useTodoStore } from "../store/todoStore";
const newTask = ref("");
const priority = ref<"Low" | "Medium" | "High">("Low");
const store = useTodoStore();
// 将直接引用改为computed属性
const filteredTodos = computed(() => store.filteredTodos);
const addNewTodo = () => {
console.log(newTask.value);
if (newTask.value.trim() === "") {
return;
}
store.addTodo(newTask.value, priority.value);
newTask.value = "";
};
const deleteTodo = (id: number) => {
store.deleteTodo(id);
};
const toggleCompletion = (id: number) => {
store.updateTodo(id, {
completed: !store.todos.find((todo) => todo.id === id)?.completed,
});
};
onBeforeMount(async () => {
await store.fetchTodos();
});
</script>后端详细实现(NestJS + Prisma + MySQL)
我们将重新整理后端实现部分,确保更完整、更清晰。后端将包含:
- 数据库模型(Prisma):创建任务表 Todo
- NestJS 模块与服务:实现增删改查(CRUD)
- API 控制器:定义 RESTful API
- 数据持久化到 MySQL
- 初始化项目并安装
创建项目:
npm i -g @nestjs/cli
nest new backend
cd backend
npm install prisma @prisma/client mysql2 // 安装 Prisma 和数据库相关依赖初始化 Prisma:
npx prisma init # 执行后会生成 prisma/schema.prisma 文件和 .env 文件。配置数据库:
# 在 .env 文件中配置数据库连接信息
DATABASE_URL="mysql://username:password@localhost:3306/todo_db"
# 替换 username 和 password 为你的 MySQL 用户名和密码,todo_db 为数据库名。在 MySQL 中创建数据库:
npx prisma db push2. 创建 Prisma 模型
打开 prisma/schema.prisma,定义 Todo 模型:
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Todo {
id Int @id @default(autoincrement())
title String
priority String
completed Boolean @default(false)
createdAt DateTime @default(now())
}保存后,运行以下命令生成 Prisma Client:
npx prisma generate3. 创建 Prisma 服务
src/prisma/prisma.service.ts:
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}4. 创建 Todo 模块
4.1 生成模块和服务:
为了快速创建内置 validation 的 CRUD 控制器,你可以使用 CLI 的 增删改查生成器:nest g resource [name]。
nest g module todo
nest g service todo
nest g controller todo4.2 Todo 服务(src/todo/todo.service.ts)
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma, Todo } from '@prisma/client';
@Injectable()
export class TodoService {
constructor(private prisma: PrismaService) {}
// 获取所有任务
async getAllTodos(): Promise<Todo[]> {
return this.prisma.todo.findMany();
}
// 创建新任务
async createTodo(data: Prisma.TodoCreateInput): Promise<Todo> {
return this.prisma.todo.create({ data });
}
// 更新任务
async updateTodo(id: number, data: Prisma.TodoUpdateInput): Promise<Todo> {
return this.prisma.todo.update({ where: { id }, data });
}
// 删除任务
async deleteTodo(id: number): Promise<Todo> {
return this.prisma.todo.delete({ where: { id } });
}
}5. 创建 Todo 控制器
src/todo/todo.controller.ts:
import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
import { TodoService } from './todo.service';
import { Prisma } from '@prisma/client';
@Controller('todos')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Get()
async getAllTodos() {
return this.todoService.getAllTodos();
}
@Post()
async createTodo(@Body() data: Prisma.TodoCreateInput) {
return this.todoService.createTodo(data);
}
@Put(':id')
async updateTodo(@Param('id') id: number, @Body() data: Prisma.TodoUpdateInput) {
return this.todoService.updateTodo(+id, data);
}
@Delete(':id')
async deleteTodo(@Param('id') id: number) {
return this.todoService.deleteTodo(+id);
}
}6. 配置主模块
src/app.module.ts:
import { Module } from '@nestjs/common';
import { TodoModule } from './todo/todo.module';
import { PrismaService } from './prisma/prisma.service';
@Module({
imports: [TodoModule],
providers: [PrismaService],
})
export class AppModule {}
7. 配置 main.ts
src/main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 启用 CORS
app.enableCors();
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();8. 启动项目
npm run start:dev此时,NestJS 应用将运行在 http://localhost:3000,并提供以下 API:
- GET /todos:获取所有任务
- POST /todos:创建新任务
- PUT /todos/:id:更新任务
- DELETE /todos/:id:删除任务