Vue3+Node.js实现一个待办事项应用

327 阅读6分钟

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:运行项目

  1. 启动后端
cd todolist-server
npm run start
  1. 启动前端
cd todolist
npm run dev

现在,你可以在浏览器中访问前端应用,并开始使用你的待办事项应用了。

微信截图_20241008153249.png

微信截图_20241008153313.png

微信截图_20241008153338.png

结语

通过这个简单的项目,我们学习了如何使用Vue 3和Node.js来构建一个完整的待办事项应用。你可以在此基础上添加更多功能。

服务端GitHub源代码地址
前端GitHub源代码地址

参考链接