Vue 3与Node.js实现待办事项应用
简介
在这个教程中,我们将一起构建一个简单的待办事项应用。前端使用Vue 3构建用户界面,后端使用Node.js和Express框架来处理数据的存储和检索。我们将从项目搭建开始,逐步实现添加、删除和标记待办事项的功能。
环境准备
在开始之前,请确保你已经安装了以下软件:
- Node.js
- VScode
项目结构
前端(Vue 3)
/todolist
|-- src
| |-- assets
| |-- api
| | |-- index.js
| |-- App.vue
| |-- main.js
|-- package.json
|-- vite.config.js
后端(Node.js)
/todolist-server
|-- models
| |-- todo.js
|-- routes
| |-- api.js
|-- index.js
|-- package.json
步骤1:搭建后端
1.初始化Nodejs
mkdir todolist-server
cd todolist-server
npm init
2.安装依赖
npm install express sqlite3@5.1.6
3.创建模型
在models/todo.js中定义待办事项的数据模型。
class DB {
constructor() {
let sqlite = require('sqlite3').verbose()
this.db = new sqlite.Database(path.join(app.getPath("appData"), 'data.db'), () => {
console.log('数据库打开成功')
this.db.run('CREATE TABLE IF NOT EXISTS todolist (id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR, state BOOLEAN, type VARCHAR, start VARCHAR, end VARCHAR, finish VARCHAR, grade VARCHAR, detail TEXT)');
})
}
// 待办模块
insert_todo(param) {
let add = this.db.prepare("INSERT OR REPLACE INTO todolist (title, state, type, start, end, finish, grade, detail) VALUES (?,?,?,?,?,?,?,?)");
add.run(param.title, param.state, param.type, param.start, param.end, param.finish, param.grade, param.detail)
add.finalize()
console.log('数据插入成功')
}
delete_todo(id) {
let del = this.db.prepare("DELETE from todolist where id =?")
del.run(id)
del.finalize()
console.log('数据删除成功')
}
update_todo(param) {
let update = this.db.prepare("UPDATE todolist set title=?, state=?, type=?, start=?, end=?, finish=?, grade=?, detail=? where id=?")
update.run(param.title, param.state, param.type, param.start, param.end, param.finish, param.grade, param.detail, param.id)
update.finalize()
console.log('数据更新成功')
}
selectAll_todo() {
return new Promise((resolve) => {
this.db.all("SELECT * FROM todolist", (err, row) => {
resolve(row)
})
})
}
select_todo(obj) {
return new Promise((resolve) => {
this.db.all("SELECT * FROM todolist", (err, row) => {
if (obj.title) {
const res = row.filter(item => {
return item.title.includes(obj.title)
})
resolve(res)
} else if (obj.type) {
const res = row.filter(item => {
return item.type === obj.type
})
resolve(res)
} else if (obj.title && obj.type) {
const res = row.filter(item => {
return item.title.includes(obj.title) && item.type === obj.type
})
resolve(res)
}
})
})
}
}
module.exports = {
DB: new DB()
}
4.创建路由
在routes/api.js中定义API路由。
const todo = require('./todo')
const express = require('express')
const db = todo.DB
const router = express.Router()
// 写入数据
router.post("/inserttodo", function (req, res) {
let body = ''
req.on('data', (thunk) => {
body += thunk
})
req.on('end', () => {
db.insert_todo(JSON.parse(body))
res.send(JSON.stringify({
code: 200
})) // 数据响应
})
})
// 更新数据
router.post("/updatetodo", function (req, res) {
let body = ''
req.on('data', (thunk) => {
body += thunk
})
req.on('end', () => {
db.update_todo(JSON.parse(body))
res.send(JSON.stringify({
code: 200
})) // 数据响应
})
})
// 查询所有数据
router.get("/selectalltodo", function (req, res) {
db.selectAll_todo().then(val => {
res.send(JSON.stringify({
code: 200,
msg: val
}))
})
})
// 筛选单条数据
router.get("/searchtodo", function (req, res) {
let obj = url.parse(req.url, true).query
db.select_todo(obj).then(val => {
res.send(JSON.stringify({
code: 200,
msg: val
}))
})
})
// 删除数据
router.get("/deletetodo", function (req, res) {
let obj = url.parse(req.url, true).query
db.delete_todo(obj.id)
res.send(JSON.stringify({
code: 200
})) // 数据响应
})
// 关闭服务
router.get("/close", function () {
process.exit(0)
})
module.exports = router
5.启动服务器
在index.js中设置Express服务器。
const express = require('express');
const app = express();
const todoRoutes = require('./api.js')
app.use('/api', todoRoutes);
const port = process.env.PORT || 4000;
app.listen(port, function () {
console.log('http://127.0.0.1:4000')
})
在package.json中添加运行命令。
"scripts": {
"start": "node index.js"
}
步骤2:搭建前端
1.初始化Vue项目
npm create vue@latest
cd todolist
npm install
2.安装依赖
npm install axios dayjs
3.安装Element-plus
npm install element-plus --save
在src/main.js引入element-plus。
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
4.创建api拦截器
在src/api目录下创建index.js。
import axios from 'axios';
const baseURL = '/api'
// const baseURL = 'http://127.0.0.1:4000'
// 创建一个 Axios 实例
const request = axios.create({
baseURL, // 你的 API 基础 URL
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
return Promise.reject(error);
}
);
export default request;
在vite.config.js中配置跨域代理。
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:4000/',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, '')
}
}
}
5.实现功能
在App.vue中实现添加和删除待办事项的功能。
<template>
<div class="container">
<div class="title">
<div>代办事项</div>
<el-dropdown>
<span class="el-dropdown-link" style="font-weight: bolder;cursor: pointer;color: #409EFF;">菜单</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="todoCreate(todoForms)">新增</el-dropdown-item>
<el-dropdown-item @click="setHide">{{ hide ? '显示' : '隐藏' }}已完成</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div style="display: flex;">
<el-input v-model="todoKeyword" size="small" style="width: 100%;margin-right: 10px;" placeholder="输入事项名称"
clearable @change="todoSearch"></el-input>
<el-select v-model="todoKeyType" size="small" placeholder="选择待办类型" clearable @change="todoSearch">
<el-option label="工作方面" value="工作方面"></el-option>
<el-option label="学习方面" value="学习方面"></el-option>
<el-option label="生活方面" value="生活方面"></el-option>
</el-select>
</div>
<el-scrollbar :height="height + 'px'" style="margin-top: 10px; padding-right: 10px;">
<div v-for="(item, index) in todoList" :key="index">
<div class="todo_item">
<el-checkbox v-model="item.state" @change="todoFinish(item)"><span class="grade">{{ item.grade
}}</span><span :style="setTime(item) ? '' : 'color:red;'">{{ item.title
}}</span></el-checkbox>
<el-popover placement="left" trigger="hover" width="200px">
<div>{{ item.detail ? item.detail : '未设置' }}</div>
<template #reference>
<el-icon>
<MoreFilled class="el-icon-more" color="#8d8d8d" @click="handleEditTodo(item)" />
</el-icon>
</template>
</el-popover>
</div>
<div class="line"></div>
</div>
</el-scrollbar>
<el-drawer v-model="drawer" direction="ltr" size="100%" :title="edit ? '编辑' : '新增'">
<el-form ref="todoForms" :model="todoForm" style="width: auto;margin: -20px 30px 0;">
<el-form-item>
<el-date-picker v-model="todoForm.start" type="datetime" clearable placeholder="选择开始时间" style="width: 100%;">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-date-picker v-model="todoForm.end" type="datetime" clearable placeholder="选择结束时间" style="width: 100%;">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-input v-model="todoForm.title" clearable placeholder="输入待办名称"></el-input>
</el-form-item>
<el-form-item>
<div style="display: flex;width: 100%;">
<el-select v-model="todoForm.type" clearable placeholder="选择分区类型">
<el-option label="工作方面" value="工作方面"></el-option>
<el-option label="学习方面" value="学习方面"></el-option>
<el-option label="生活方面" value="生活方面"></el-option>
</el-select>
<el-select v-model="todoForm.grade" clearable placeholder="选择优先级">
<el-option label="Ⅰ级" value="Ⅰ级"></el-option>
<el-option label="Ⅱ级" value="Ⅱ级"></el-option>
<el-option label="Ⅲ级" value="Ⅲ级"></el-option>
<el-option label="Ⅳ级" value="Ⅳ级"></el-option>
</el-select>
</div>
</el-form-item>
<el-form-item>
<el-input type="textarea" v-model="todoForm.detail" :autosize="{ minRows: 8, maxRows: 8 }"
placeholder="输入待办详情"></el-input>
</el-form-item>
<el-form-item>
<div style="text-align: right;width: 100%;">
<el-button type="primary" size="small" @click="todoSave">保存</el-button>
<el-button v-if="edit" type="danger" size="small" @click="todoRemove">删除</el-button>
</div>
</el-form-item>
</el-form>
<div class="finish" v-show="todoForm.finish">完成时间:{{ todoForm.finish }}</div>
</el-drawer>
</div>
</template>
<script setup>
import { MoreFilled } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { computed, onBeforeMount, reactive, ref } from "vue";
import dayjs from "dayjs";
import request from "./api";
const drawer = ref(false)
const edit = ref(false)
const hide = ref(false)
const todoForm = reactive({
id: '',
title: '',
state: false,
detail: '',
type: '',
start: '',
end: '',
finish: '',
grade: ''
})
const todoList = ref([])
const todoKeyword = ref("")
const todoKeyType = ref("")
const todoForms = ref(null)
const height = ref(0)
onBeforeMount(() => {
getTodoList()
height.value = window.innerHeight - 65
})
const setTime = computed(() => {
return function (val) {
if (!val.end) return true
if (val.state) return true
return parseInt(dayjs(val.end).format('DD')) >= parseInt(dayjs().format('DD'))
}
})
function handleEditTodo(item) {
todoForm.detail = item.detail
todoForm.end = item.end
todoForm.finish = item.finish
todoForm.grade = item.grade
todoForm.start = item.start
todoForm.state = item.state
todoForm.title = item.title
todoForm.type = item.type
todoForm.id = item.id
edit.value = true
drawer.value = true
}
function getTodoList() {
request.get('/selectalltodo').then((res) => {
let finList = []
let unfinList = []
res.msg.forEach(item => {
item.state = item.state === 0 ? false : true
if (item.state) {
finList.push(item)
} else {
unfinList.push(item)
}
})
if (hide.value) {
todoList.value = unfinList
} else {
todoList.value = [...unfinList, ...finList]
}
}).catch((err) => ElMessage.error(JSON.stringify(err)))
}
function todoCreate() {
todoForm.detail = ""
todoForm.end = ""
todoForm.finish = ""
todoForm.grade = ""
todoForm.start = ""
todoForm.state = false
todoForm.title = ""
todoForm.type = ""
todoForm.id = ""
edit.value = false
drawer.value = true
}
function todoSave() {
if (!edit.value) {
request.post('/inserttodo', JSON.stringify(todoForm)).then(() => {
ElMessage({
type: 'success',
message: '保存成功!',
showClose: true
});
}).catch(() => ElMessage.error('提交失败'))
} else {
request.post('/updatetodo', JSON.stringify(todoForm)).then(() => {
ElMessage({
type: 'success',
message: '保存成功!',
showClose: true
});
}).catch(() => ElMessage.error('提交失败'))
}
getTodoList()
drawer.value = false
}
function todoRemove() {
request.get(`/deletetodo?id=${todoForm.id}`).then(() => {
ElMessage({
type: 'success',
message: '删除成功!',
showClose: true
});
getTodoList()
drawer.value = false
}).catch(() => ElMessage.error('删除失败'))
}
function todoFinish(item) {
item.finish = item.state ? dayjs().format('YYYY-MM-DD HH:mm:ss') : ''
request.post('/updatetodo', JSON.stringify(item)).then(() => {
ElMessage({
type: 'success',
message: item.state ? '事件已完成!' : '已取消',
showClose: true
});
getTodoList()
}).catch(() => ElMessage.error('提交失败'))
}
function todoSearch() {
hide.value = false
if (!todoKeyword.value && !todoKeyType.value) {
getTodoList()
} else {
request.get(`/searchtodo?title=${todoKeyword.value}&type=${todoKeyType.value}`).then((res) => {
let finList = []
let unfinList = []
res.msg.forEach(item => {
item.state = item.state === 0 ? false : true
if (item.state) {
finList.push(item)
} else {
unfinList.push(item)
}
})
todoList.value = [...unfinList, ...finList]
}).catch((err) => ElMessage.error(JSON.stringify(err)))
}
}
function setHide() {
hide.value = !hide.value
getTodoList()
}
</script>
<style scoped>
.container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 0 10px 10px;
}
.title {
display: flex;
justify-content: space-between;
margin: 5px;
align-items: center;
}
.el_icon {
padding: 4px 5px;
color: #363a69;
transition-duration: 500ms;
}
.el_icon:hover {
cursor: pointer;
background: #e6e6e6;
transition-duration: 500ms;
}
.url_table {
width: 100%;
}
.name {
text-decoration: none;
font-weight: bold;
color: rgb(43, 128, 226);
}
.menu {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 180px;
}
.item {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 0;
width: 25%;
cursor: pointer;
}
.title {
color: #666666;
font-weight: bold;
font-size: 16px;
margin-top: 5px;
}
.lnk {
display: flex;
flex-wrap: wrap;
}
.icon {
width: 80px;
height: 80px;
color: #ffffff;
text-shadow: 2px 1px 2px rgb(138, 120, 120);
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
white-space: nowrap;
}
.key_search {
display: block;
width: 250px;
margin: 20px auto 100px;
}
.header {
position: absolute;
top: 15px;
right: 15px;
text-align: right;
z-index: 1;
}
.study {
text-decoration: none;
text-align: right;
color: #8d8d8d;
}
.todo_item {
display: flex;
justify-content: space-between;
align-items: center;
}
.grade {
font-size: 12px;
color: #8d8d8d;
margin: 0 5px 0 -5px;
}
.el-icon-more,
.el-icon-edit {
cursor: pointer;
}
.line {
border-bottom: 1px solid #d8d8d8;
margin: 5px 0;
}
.finish {
position: absolute;
right: 10px;
bottom: 5px;
font-size: 14px;
color: #8d8d8d;
}
</style>
步骤3:运行项目
- 启动后端
cd todolist-server
npm run start
- 启动前端
cd todolist
npm run dev
现在,你可以在浏览器中访问前端应用,并开始使用你的待办事项应用了。
结语
通过这个简单的项目,我们学习了如何使用Vue 3和Node.js来构建一个完整的待办事项应用。你可以在此基础上添加更多功能。