【前后端案例】vue3+express实现简易版学生信息管理系统【文章附有完整代码】

一、前言

一般做前端很少直接接触后端服务和数据的事情,为了打破这种限制,同时也是一种尝试,因此我做了这个案例。本案例前端是选择vue3,状态管理库是vuex。

二、有哪些功能需求?

1、前端:

  • 登录
  • 注销
  • 学生信息的增删改查功能

2、后端:

需要用到的第三方库

三、代码实现

注意:在此不演示vue3项目的创建过程,有需要请自行查找了解。

本次案例主要是给思路,展示部分代码,但部分文件的创建以及代码撰写,需要大家动手实现。

1、安装项目需要的包:

npm install axios bcryptjs body-parser cors express jsonwebtoken

 bcryptjs:主要是用于对登录的密码进行哈希加密

2、后端server.js

用于撰写接口,鉴权以及启动服务等。

const express = require('express');
const cors = require('cors');
const data = require('./data');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const app = express();

// 验证JWT令牌的中间件
const authenticate = (req, res, next) => {
  const token = req.header('Authorization').replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ message: 'Access denied. No token provided.' });
  }

  try {
    const decoded = jwt.verify(token, 'your_jwt_secret');
    req.user = decoded;
    next();
  } catch (ex) {
    res.status(400).json({ message: 'Invalid token.' });
  }
};

// 获取所有学生信息
app.get('/api/students', authenticate, (req, res) => {
  res.json(data.students);
});

// 添加学生信息
app.post('/api/students', authenticate, (req, res) => {
  const { name, age } = req.body;
  const newStudent = { id: Date.now(), name, age };
  data.students.push(newStudent);
  res.status(201).json(newStudent);
});

// 更新学生信息
app.put('/api/students/:id', authenticate, (req, res) => {
  const { id } = req.params;
  const { name, age } = req.body;
  const studentIndex = data.students.findIndex(s => s.id.toString() === id);
  if (studentIndex === -1) {
    return res.status(404).json({ message: 'Student not found' });
  }
  data.students[studentIndex] = { ...data.students[studentIndex], name, age };
  res.json(data.students[studentIndex]);
});

// 删除学生信息
app.delete('/api/students/:id', authenticate, (req, res) => {
  const { id } = req.params;
  const studentIndex = data.students.findIndex(s => s.id.toString() === id);
  if (studentIndex === -1) {
    return res.status(404).json({ message: 'Student not found' });
  }
  data.students.splice(studentIndex, 1);
  res.json({ message: 'Student deleted successfully' });
});

// 用户登录
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  const user = data.users.find(u => u.username === username);

  if (!user) {
    return res.status(400).json({ message: 'User not found' });
  }

  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) {
    return res.status(400).json({ message: 'Invalid credentials' });
  }

  const token = jwt.sign({ id: user.id }, 'your_jwt_secret', { expiresIn: '1h' });
  res.json({ token });
});

// 使用中间件
app.use(cors());
app.use(express.json());

// 全局错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: 'Internal Server Error' });
});

// 启动服务
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

 3、data.js:

用于存储学生信息数据以及用户数据

const bcrypt = require('bcryptjs')

const hashedPassword = bcrypt.hashSync('123', 10) // 生成哈希密码

module.exports = {
  students: [
    { id: 1, name: '张三', age: 20 },
    { id: 2, name: '李四', age: 21 },
    { id: 3, name: '王五', age: 22 },
  ],
  users: [
    { id: 1, username: 'admin', password: hashedPassword }, // 使用哈希密码
    { id: 2, username: 'user', password: hashedPassword }, // 使用哈希密码
  ],
}

4、运行调试:

在后端项目目录下打开终端,执行node server.js

看到这条语句则表示后端服务启动成功。

5、前端代码:

src/components/StudentList.vue:(学生信息展示页面)

<template>
  <div>
    <h1>学生信息列表</h1>
    <ul>
      <li v-for="student in students" :key="student.id">
        {
  
  { student.name }} - {
  
  { student.age }}岁
        <button @click="editStudent(student)">编辑</button>
        <button @click="deleteStudent(student.id)">删除</button>
      </li>
    </ul>
    <button @click="logout">注销</button>
    <!-- 添加学生表单 -->
    <div>
      <h2>添加学生</h2>
      <form @submit.prevent="addStudent">
        <div>
          <label for="name">姓名:</label>
          <input type="text" id="name" v-model="newStudent.name" required />
        </div>
        <div>
          <label for="age">年龄:</label>
          <input type="number" id="age" v-model="newStudent.age" required />
        </div>
        <button type="submit">添加</button>
      </form>
    </div>

    <!-- 编辑学生模态框 -->
    <div v-if="editingStudent" class="modal">
      <div class="modal-content">
        <span class="close" @click="cancelEdit">&times;</span>
        <h2>编辑学生信息</h2>
        <form @submit.prevent="updateStudent">
          <div>
            <label for="edit-name">姓名:</label>
            <input type="text" id="edit-name" v-model="editingStudent.name" required />
          </div>
          <div>
            <label for="edit-age">年龄:</label>
            <input type="number" id="edit-age" v-model="editingStudent.age" required />
          </div>
          <button type="submit">保存</button>
          <button type="button" @click="cancelEdit">取消</button>
        </form>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, ref } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
const router = useRouter()
const store = useStore()

const students = computed(() => store.getters.getStudents)

const newStudent = ref({ name: '', age: '' })
const editingStudent = ref(null)

const addStudent = async () => {
  try {
    await store.dispatch('addStudent', newStudent.value)
    newStudent.value = { name: '', age: '' } // 清空表单
  } catch (error) {
    console.error('添加学生失败:', error)
  }
}

const editStudent = (student) => {
  editingStudent.value = { ...student }
}

const updateStudent = async () => {
  try {
    await store.dispatch('updateStudent', editingStudent.value)
    editingStudent.value = null // 关闭模态框
  } catch (error) {
    console.error('更新学生失败:', error)
  }
}

const cancelEdit = () => {
  editingStudent.value = null // 关闭模态框
}
const fetchStudents = () => {
  store.dispatch('fetchStudents')
}

const logout = () => {
  store.dispatch('logout')
  router.replace('/')
}
const deleteStudent = async (id) => {
  try {
    await store.dispatch('deleteStudent', id)
  } catch (error) {
    console.error('删除学生失败:', error)
  }
}
fetchStudents() // 在组件创建时获取学生信息
</script>

<style scoped>
.modal {
  position: fixed;
  z-index: 1;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto;
  background-color: rgb(0, 0, 0);
  background-color: rgba(0, 0, 0, 0.4);
}

.modal-content {
  background-color: #fefefe;
  margin: 15% auto;
  padding: 20px;
  border: 1px solid #888;
  width: 80%;
  max-width: 500px;
}

.close {
  color: #aaa;
  float: right;
  font-size: 28px;
  font-weight: bold;
}

.close:hover,
.close:focus {
  color: black;
  text-decoration: none;
  cursor: pointer;
}
</style>

src/components/Login.vue:(登录页面)

<template>
  <div>
    <h1>登录</h1>
    <form @submit.prevent="handleLogin">
      <div>
        <label for="username">用户名:</label>
        <input type="text" id="username" v-model="username" required />
      </div>
      <div>
        <label for="password">密码:</label>
        <input type="password" id="password" v-model="password" required />
      </div>
      <button type="submit">登录</button>
    </form>
    <p v-if="error" class="error">{
  
  { error }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'

const store = useStore()
const router = useRouter()

const username = ref('')
const password = ref('')
const error = ref('')

const handleLogin = async () => {
  try {
    await store.dispatch('login', { username: username.value, password: password.value })
    console.log('验证成功了', username.value, password.value, router)

    router.replace('/students') // 成功登录后跳转到学生列表页面
  } catch (error) {
    error.value = '登录失败,请检查用户名和密码。'
  }
}
</script>

<style>
.error {
  color: red;
}
</style>

src/store/index.js:

import { createStore } from 'vuex'
import axios from 'axios'

const apiClient = axios.create({
  baseURL: 'http://localhost:5000/api',
  withCredentials: false,
  // withCredentials: 这个选项决定了是否在跨域请求中发送凭据(如 cookies、HTTP 认证信息等)。
  // 默认值是 false,表示不发送凭据。
  headers: {
    Accept: 'application/json',  //客户端期望接收 JSON 格式的数据。
    'Content-Type': 'application/json',  //这个头告诉服务器,客户端发送的数据是 JSON 格式的
  },
})

// vuex用法
export default createStore({
  state: {
    students: [],
    token: localStorage.getItem('token') || null,
  },
  mutations: {
    setStudents(state, students) {
      state.students = students
    },
    setToken(state, token) {
      state.token = token
      localStorage.setItem('token', token)
    },
    clearToken(state) {
      state.token = null
      localStorage.removeItem('token')
    },
    addStudent(state, student) {
      state.students.push(student)
    },
    removeStudent(state, id) {
      state.students = state.students.filter((student) => student.id !== id)
    },
    updateStudent(state, updatedStudent) {
      state.students = state.students.map((student) =>
        student.id === updatedStudent.id ? updatedStudent : student,
      )
    },
  },
  actions: {
    // 获取学生信息数据
    async fetchStudents({ commit, state }) {
      try {
        const response = await apiClient.get('/students', {
          headers: {
            Authorization: `Bearer ${state.token}`,
          },
        })
        commit('setStudents', response.data)
      } catch (error) {
        console.error('There was an error fetching the students!', error)
      }
    },
    // 登录逻辑
    async login({ commit }, { username, password }) {
      try {
        const response = await apiClient.post('/login', { username, password })
        // 将登录成功得到的token存储在本地
        commit('setToken', response.data.token)
      } catch (error) {
        console.error('Login failed!', error)
        throw error // 重新抛出错误,以便在组件中捕获
      }
    },
    // 退出登录
    logout({ commit }) {
      console.log('退出了')
      commit('clearToken')
    },
    // 添加学生 
    addStudent({ commit, state }, newStudent) {
      return apiClient
        .post('/students', newStudent, {
          headers: {
            Authorization: `Bearer ${state.token}`,
          },
        })
        .then((response) => {
          commit('addStudent', response.data)
        })
    },
    // 编辑/更新学生
    updateStudent({ commit, state }, updatedStudent) {
      return apiClient
        .put(`/students/${updatedStudent.id}`, updatedStudent, {
          headers: {
            Authorization: `Bearer ${state.token}`,
          },
        })
        .then(() => {
          commit('updateStudent', updatedStudent)
        })
    },
    // 删除学生信息
    deleteStudent({ commit, state }, id) {
      return apiClient
        .delete(`/students/${id}`, {
          headers: {
            Authorization: `Bearer ${state.token}`,
          },
        })
        .then(() => {
          commit('removeStudent', id)
        })
    },
  },
  getters: {
    getStudents: (state) => state.students,
    isLoggedIn: (state) => !!state.token,
  },
})

 src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import Login from '../components/Login.vue'
import StudentList from '../components/StudentList.vue'

const routes = [
  { path: '/', component: Login },
  { path: '/students', component: StudentList },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

router.beforeEach((to, from, next) => {
  if (to.path === '/students' && !localStorage.getItem('token')) {
    next('/')
  } else {
    next()
  }
})

export default router

 src/App.js

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script setup>
</script>

 src/main.js

import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router/index'
import store from './store/index'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(store)

app.mount('#app')

四、结果演示

注意:运行程序的时候,要确保后端文件server.js是启动的(也就是你打开的执行node server.js的终端不要关闭)

1、登录功能:

用户名:admin

密码:123

2、删除功能:

3、添加功能:

编辑功能:

 五、后记

本次案例是使用了vuex作为状态管理库,主要是测试vuex知识点。这里也推荐大家使用pinia,比较容器上手。

后续再考虑将vuex迁移成pinia。


如果你喜欢这篇文章,请点赞收藏。

关注我,了解更多实用的知识和技术~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十八朵郁金香

感恩前行路上有你相伴

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值