vue3+node.js+mysql写接口(一)

发布于:2025-07-08 ⋅ 阅读:(18) ⋅ 点赞:(0)

目录

一、项目准备

1.1、安装创建vue3项目

1.2、前端配置

1.3、创建Express后端项目

二、用户模块(user表)

2.1、登录(/adminapi/user/login)

2.2、个人中心(/adminapi/user/upload)

2.3、添加用户(/adminapi/user/add)

2.4、用户列表(/adminapi/user/list)

2.5、删除用户(/adminapi/user/delete/:id)

2.6、编辑用户(/adminapi/user/update)

2.7、用户权限控制

三、新闻模块(news)

3.1、创建新闻(/adminapi/news/add)

3.2、新闻列表(/adminapi/news/list)

3.2.1、分页实现

3.2.2、时间渲染

3.3、发布新闻(/adminapi/news/publish)

3.4、预览新闻(/adminapi/news/detail/:id)

3.5、删除新闻(/adminapi/news/delete/:id)

3.7、编辑新闻(/adminapi/news/detail/:id)

一、项目准备

前言:

这是一个用vue3+node.js+mysql实现的三端项目:分为后台管理系统、web端展示、后端操作数据库,包含用户管理、新闻管理、产品管理三大模块,其中,web端就是直接展示后台管理系统中用户添加的数据。

1.1、安装创建vue3项目

(1)、npm install -g @vue/cli-service-global (看自己有没有安装)

(2)、vue create admin (admin是项目名,随自己)

(3)、Manually select features后用空格键盘选中需要的features:Babel、Router、Vuex、CSS Pre-processors,选择3.x的version

(4)、创好项目,建立路由后:npm install element-plus --save

1.2、前端配置

登录页面的运动粒子效果:npm i particles.vue3  使用教程 :particles.vue3 - npm

引入组件报错,使用npm install tsparticles-slim进行解决

    <!-- 粒子背景 -->
    <vue-particles
      id="tsparticles"
      :particlesInit="particlesInit"
      :particlesLoaded="particlesLoaded"
      :options="particlesOptions"
    />
<script setup>
import { loadSlim } from "tsparticles-slim";
// 粒子配置
const particlesOptions = {
  background: {
    color: {
      value: "#9c64a7",
    },
  },
  fpsLimit: 120,
  interactivity: {
    events: {
      onClick: {
        enable: true,
        mode: "push",
      },
      onHover: {
        enable: true,
        mode: "repulse",
      },
      resize: true,
    },
    modes: {
      bubble: {
        distance: 400,
        duration: 2,
        opacity: 0.8,
        size: 40,
      },
      push: {
        quantity: 4,
      },
      repulse: {
        distance: 200,
        duration: 0.4,
      },
    },
  },
  particles: {
    color: {
      value: "#ffffff",
    },
    links: {
      color: "#ffffff",
      distance: 150,
      enable: true,
      opacity: 0.5,
      width: 1,
    },
    collisions: {
      enable: true,
    },
    move: {
      direction: "none",
      enable: true,
      outModes: {
        default: "bounce",
      },
      random: false,
      speed: 6,
      straight: false,
    },
    number: {
      density: {
        enable: true,
        area: 800,
      },
      value: 80,
    },
    opacity: {
      value: 0.5,
    },
    shape: {
      type: "circle",
    },
    size: {
      value: { min: 1, max: 5 },
    },
  },
  detectRetina: true,
};
const particlesInit = async (engine) => {
  await loadSlim(engine);
};
const particlesLoaded = async (container) => {
  console.log("Particles container loaded", container);
};
</script>
<style scoped>
#apps {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}
#tsparticles {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
}
</style>

vuex持久化:npm install vuex-persistedstate --save

在store的index.js文件里

import createPersistedState from "vuex-persistedstate";
  // 与 state、mutations 同级
  plugins: [
    createPersistedState({
      paths: ["isCollapsed", "userInfo"], //只让它持久化
    }),
  ],

axios下载:npm i axios

在api的axios.js文件里

import Axios from "axios";
const axios = Axios.create({
  // baseURL: import.meta.env.VITE_API_BASE,
  timeout: 300000,
});

axios.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem("token");
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    Promise.reject(error);
  }
);
axios.interceptors.response.use(
  (response) => {
    // 1. 判断响应码
    const data = response.data;
    const { authorization } = response.headers;
    authorization && localStorage.setItem("token", authorization);
    return data;
  },
  (error) => {
    const { status } = error.response;
    if (status == 401) {
      localStorage.removeItem("token");
      window.location.href = "#/login";
    }
    return Promise.reject(error);
  }
);
export default axios;

1.3、创建Express后端项目

npm install mysql2 【连接数据库】

在db.js里

const mysql = require("mysql2");
// 创建数据库连接配置
const connection = mysql.createConnection({
  host: "localhost", // 数据库主机地址
  user: "root", // 数据库用户名
  password: "123456", // 数据库密码
  database: "my-school", // 数据库名称
  port: 3307, // 数据库端口号,默认为3306
});
// 连接数据库
connection.connect((err) => {
  if (err) {
    console.log("MySQL连接失败:", err);
  } else {
    console.log("MySQL数据库连接成功");
  }
});
module.exports = connection;

npm install jsonwebtoken【登录成功后返回的token,并校验token的存在与更新】

在项目util文件夹的JWT.js

const jsonwebtoken = require("jsonwebtoken");
const secret = "my_super_secret_key_2024!@#";
const JWT = {
  generate(value, expires) {
    return jsonwebtoken.sign(value, secret, { expiresIn: expires });
  },
  verify(token) {
    try {
      return jsonwebtoken.verify(token, secret);
    } catch (err) {
      return false;
    }
  },
};
module.exports = JWT;

配置参考:node.js连接mysql写接口(一)_node.js 连接数据库,写一个查询接口-CSDN博客

每一个接口的完整流程:

  先在routes里写接口,在controlles里处理接口逻辑,再在services里处理sql,改变或者获取数据库表里的数据。

二、用户模块(user表)

连接数据库后,先建一个user表:

-- 创建users表
CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE,
  password VARCHAR(255) NOT NULL,
  gender INT DEFAULT 0,
  introduction TEXT,
  avatar VARCHAR(255),
  role INT DEFAULT 2, -- 管理员 1,编辑 2
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 插入测试用户数据
INSERT INTO users (username, password, gender, introduction, avatar, role) VALUES
('admin', '123456', 1, '系统管理员', '/avatar/admin.jpg', 1),
('editor', '123456', 0, '内容编辑', '/avatar/editor.jpg', 2),
('test', 'test', 1, '测试用户', '/avatar/test.jpg', 2); 

2.1、登录(/adminapi/user/login)

前端传递用户名和密码,后端接收并判断是否在数据库里存在,相当于数据库表user的指定信息查询,验证成功后返回用户信息,并且通过JWT,每24h更新一次token,将返回的token和userInfo信息存在vuex里,退出登陆时进行清空。

后端代码解释:

下方是controlles文件夹里的admin中的login方法

  login: async (req, res) => {
    try {
      console.log("收到登录请求:", req.body); //{ username: 'admin', password: '123456' }
      const { username, password } = req.body;
      // 参数验证
      if (!username || !password) {
        return res.status(400).json({
          code: -1,
          message: "用户名和密码不能为空",
        });
      }
      // 查询用户
      const result = await UserService.login({ username, password });
      console.log("数据库查询结果:", result); //result数组里是admin的所有信息
      if (result.length === 0) {
        return res.json({
          code: 500,
          message: "用户名或密码错误",
        });
      }
      const user = result[0];
      // 生成token
      const token = JWT.generate(
        {
          id: user.id,
          username: user.username,
          role: user.role,
        },
        "24h"
      );
      res.header("Authorization", token);
      // 返回成功响应
      res.json({
        code: 200,
        message: "登录成功",
        data: {
          token,
          userInfo: {
            id: user.id,
            username: user.username,
            role: user.role,
            gender: user.gender || 0,
            introduction: user.introduction,
            avatar: user.avatar,
          },
        },
      });
    } catch (error) {
      console.log("登录接口错误:", error);
      res.status(500).json({
        code: -1,
        message: "服务器内部错误",
      });
    }
  },

前端处理逻辑:

const submitForm = (formEl) => {
  if (!formEl) return;
  formEl.validate((valid) => {
    if (valid) {
      axios
        .post("/adminapi/user/login", loginForm)
        .then((res) => {
          if (res.code == 200) {
            // localStorage.setItem("token", res.data.token);
            // localStorage.setItem(
            //   "USER_INFO",
            //   JSON.stringify(res.data.userInfo)
            // );
            store.commit("changeUserInfo", res.data.userInfo);
            router.push("/index");
          } else {
            ElMessage.error(res.message);
          }
        })
        .catch((err) => {
          console.log(err);
        });
    } else {
      console.log("error submit!");
    }
  });
};

登录成功渲染首页:首页头像和时间判断都是通过computed进行

2.2、个人中心(/adminapi/user/upload)

拿的是vuex持久化插件保存的信息,这里牵扯到个人信息上传,相当于数据库表user的信息修改【UPDATE users SET】,前端传递FormData格式数据到后端,此时后端需要下载multer【github.com里搜索下载】来处理multipart/form-data类型的表单数据,并判断前端是否传递了file二进制文件,进行非空校验,并返回修改后的信息。

前端逻辑:

const uploadChange = (file) => {
  userForm.avatar = URL.createObjectURL(file.raw);
  userForm.file = file.raw; //file文件【二进制】格式,传给后端时用FormData形式
};
const submitForm = async (formEl) => {
  if (!formEl) return;
  await formEl.validate(async (valid, fields) => {
    if (valid) {
      const res = await upload("adminapi/user/upload", userForm);
      if (res.code === 200) {
        store.commit("changeUserInfo", res.data);
        ElMessage.success(res.message);
      }
    } else {
      console.log("error submit!", fields);
    }
  });
};

公共上传方法:upload(在前端的utils文件夹里,引入即可)

import axios from '../api/axios'
function upload(path, userForm) {
  const params = new FormData();
  for (let key in userForm) {
    params.append(key, userForm[key]);
  }
  return axios
    .post(path, params, {
      headers: { "Content-Type": "multipart/form-data" },
    })
    .then((res) => res);
}
export default upload;

后端逻辑:

// 图片上传
const multer = require("multer");
//在项目中的public建立一个avataruploads文件夹
const upload = multer({ dest: "public/avataruploads/" });
UserRouter.post(
  "/adminapi/user/upload",
  upload.single("file"),
  UserController.upload
);

下方是controlles文件夹里的admin中的upload方法

  upload: async (req, res) => {
    const { username, introduction, gender } = req.body;
    const token = req.headers["authorization"]?.split(" ")[1];
    // 如果前端未传递图像的file数据
    const avatar = req.file ? `/avataruploads/${req.file.filename}` : "";
    let payload;
    if (token) {
      payload = JWT.verify(token);
      const updateData = {
        id: payload.id,
        username,
        introduction,
        gender: Number(gender),
        avatar,
      };
      try {
        await UserService.upload(updateData);
        res.send({
          code: 200,
          data: {
            username,
            introduction,
            gender: Number(gender),
            avatar: req.file ? avatar : undefined,
          },
          message: "修改成功",
        });
      } catch (error) {
        res.status(500).send({
          code: 500,
          message: "用户信息更新失败",
        });
      }
    } else {
      res.status(401).send({
        code: 401,
        message: "未授权",
      });
    }
    console.log(req.body, "req.file:", req.file, payload && payload.id);
  },
// services里核心sql
const sql =
 "UPDATE users SET username = ?, introduction = ?, gender = ?, avatar = ? WHERE id = ?";

让前端更新vuex里的信息,从而达到全局信息替换的目的,此时前端将上传图片组件进行封装:提取upload组件并将上传方法进行封装,便于二次使用。

<template>
  <el-upload
    class="avatar-uploader"
    action=""
    :show-file-list="false"
    :auto-upload="false"
    :on-change="handleChange"
  >
    <img v-if="avatar" :src="backUrl" class="avatar" />
    <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
  </el-upload>
</template>
<script setup>
import { Plus } from "@element-plus/icons-vue";
import { defineEmits, defineProps, computed } from "vue";
const props = defineProps({
  avatar: {
    type: String,
    default: "",
  },
});
const backUrl = computed(() =>
  props.avatar.includes("blob")
    ? props.avatar
    : "http://localhost:3000" + props.avatar
);
const emit = defineEmits(["uploadChange"]);
const handleChange = (file) => {
  emit("uploadChange", file);
};
</script>
<style lang="scss" scoped>
.avatar-uploader .avatar {
  width: 178px;
  height: 178px;
  display: block;
}
::v-deep .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}

::v-deep .el-upload:hover {
  border-color: var(--el-color-primary);
}

::v-deep .el-icon.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  text-align: center;
}
</style>

2.3、添加用户(/adminapi/user/add)

前端数据基本同个人中心,只是没有id,这里就是将前端的数据插入【INSERT INTO】数据库表users,总列表数也随之增加一个。

后端sql核心代码

  addList: async ({
    username,
    introduction,
    gender,
    role,
    password,
    avatar,
  }) => {
    return new Promise((resolve, reject) => {
      const sql =
        "INSERT INTO users (username,introduction,gender,role,password,avatar) VALUES (?,?,?,?,?,?)";
      connection.query(
        sql,
        [username, introduction, gender, role, password, avatar],
        (err, results) => {
          if (err) {
            console.log("添加用户信息错误:", err);
            reject(err);
          } else {
            resolve(results);
          }
        }
      );
    });
  },

2.4、用户列表(/adminapi/user/list)

相当于数据库表users的查询【SELECT * FROM users】,并将总列表数返回给前端,前端直接用表格进行渲染。

  getList: async () => {
    return new Promise((resolve, reject) => {
      const sql = "SELECT * FROM users";
      connection.query(sql, (err, results) => {
        if (err) {
          console.log("查询错误:", err);
          reject(err);
        } else {
          console.log("查询结果:", results);
          resolve(results);
        }
      });
    });
  },

2.5、删除用户(/adminapi/user/delete/:id)

根据id删除【DELETE FROM users】数据库表users中的相关信息,总列表数也随之减少一个。

  delList: async (id) => {
    return new Promise((resolve, reject) => {
      const sql = "DELETE FROM users WHERE id = ?";
      connection.query(sql, [id], (err, results) => {
        if (err) {
          console.log("删除用户错误:", err);
          reject(err);
        } else {
          resolve(results);
        }
      });
    });
  },

2.6、编辑用户(/adminapi/user/update)

参考个人中心,这里在编辑弹窗回显时存在ref和reactive赋值会改变原始数据问题。

const userForm = reactive({
  username: "",
  password: "",
  role: 2, //1 管理员 2 编辑
  introduction: "",
});
const editRow = (row) => {
  // 如果userForm是ref对象,直接赋值,会改变原始数据tableData,需要深拷贝
  // 1、简单对象:userForm.value = JSON.parse(JSON.stringify(row));
  // 2、复杂对象:使用lodash的_.cloneDeep创建深拷贝【npm install lodash后引入import _ from 'lodash';】
  //             userForm.value = _.cloneDeep(row);

  // 如果userForm是reactive对象,直接赋值,会改变原始数据tableData
  // 1、使用toRaw将reactive对象转换为普通对象,进行深拷贝后再将其转换回reactive对象
  //    const rawRow = toRaw(row);
  //    const copiedRow = _.cloneDeep(rawRow);
  //    userForm = reactive(copiedRow);
  // 2、直接Object.assign(userForm,row)
  Object.assign(userForm, row);
  dialogFormVisible.value = true;
};

后端核心代码

  updateList: async ({ id, username, introduction, role, password }) => {
    return new Promise((resolve, reject) => {
      const sql =
        "UPDATE users SET username = ?, introduction = ?, role = ?, password = ? WHERE id = ?";
      connection.query(
        sql,
        [id, username, introduction, role, password],
        (err, results) => {
          if (err) {
            console.log("修改用户信息错误:", err);
            reject(err);
          } else {
            resolve(results);
          }
        }
      );
    });
  },

2.7、用户权限控制

界面菜单用v-admin指令控制,但是还要处理路由:将需要控制的路由加上requireAdmin属性,在router的index.js加上判断,注意在登陆时将 store.commit("changeGetterRouter", false);这样就防止编辑人员通过url直接访问到管理人员菜单和页面。

// 界面删除 ,还需要加上路由删除
const vAdmin = {
  mounted(el) {
    // 判断当前用户是否为管理员,不是的话就从父节点中移除当前元素(el)
    if (store.state.userInfo.role !== 1) {
      el.parentNode.removeChild(el);
    }
  },
};
import { createRouter, createWebHashHistory } from "vue-router";
import Login from "../views/Login.vue";
import MainBox from "../views/MainBox.vue";
import RoutesConfig from "./config";
import store from "../store/index";
const routes = [
  {
    path: "/login",
    name: "login",
    component: Login,
  },
  {
    path: "/mainbox",
    name: "mainbox",
    component: MainBox,
  },
  // mainbox的嵌套路由,后面根据权限动态添加
];
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 配置动态路由的函数
const ConfigRouter = () => {
  if (!router.hasRoute("mainbox")) {
    router.addRoute({
      path: "/mainbox",
      name: "mainbox",
      component: MainBox,
    });
  }
  RoutesConfig.forEach((item) => {
    checkPermission(item) && router.addRoute("mainbox", item);
  });
  // 改变isGetterRouter=true
  store.commit("changeGetterRouter", true);
};
const checkPermission = (item) => {
  if (item.requireAdmin) {
    return store.state.userInfo.role === 1;
  } else {
    return true;
  }
};
// 每次路由跳转之前
router.beforeEach((to, from, next) => {
  if (to.name === "login") {
    next();
  } else {
    // 未授权,重定向到login
    if (!localStorage.getItem("token")) {
      next({ name: "login" });
    } else {
      if (!store.state.isGetterRouter) {
        // 删除所有的嵌套路由
        router.removeRoute("mainbox");
        ConfigRouter();
        // 重新导航到目标路由,确保新添加的路由生效
        next({ ...to, replace: true });
      } else {
        next();
      }
    }
  }
});
export default router;

三、新闻模块(news)

建一个名为news的表:

-- 创建news表
CREATE TABLE IF NOT EXISTS news (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(50) NOT NULL UNIQUE,
  content LONGTEXT NOT NULL,-- 超大文本
  category INT DEFAULT 0,
  cover VARCHAR(255),
  isPublish INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  edit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FULLTEXT INDEX idx_content (content) -- 添加全文索引(如果支持中文搜索)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 解释
-- 1、使用InnoDB引擎:为了事务支持和数据完整性。
-- 2、使用utf8mb4字符集:为了全面支持Unicode字符(包括中文生僻字、emoji、多语言等)。
-- 3、使用utf8mb4_unicode_ci排序规则:为了更准确的排序和比较

-- 插入测试新闻数据
INSERT INTO news (title, content, category, cover, isPublish) VALUES
('标题1', '富文本内容...', 1, '/avatar/admin.jpg', 0),
('editor', '123456...', 0, '/avatar/editor.jpg', 0),

3.1、创建新闻(/adminapi/news/add)

富文本编辑器:Introduction · wangEditor 用户文档

下载v4版本:npm i wangeditor --save

前端编辑器组件如下,其中的封面上传参考个人中心,注意在提交时将当前用户加入,为了在渲染列表时只看到当前用户创建的,后端接收数据后,添加进news表即可

编辑器组件(前端):

<template>
  <div id="myeditor"></div>
</template>
<script setup>
import { onMounted, defineEmits, defineProps } from "vue";
import E from "wangeditor";
const emits = defineEmits(["change"]);
const props = defineProps({
  content: String,
});
onMounted(() => {
  const editor = new E("#myeditor");
  editor.create();
  // 设置初始值
  props.content && editor.txt.html(props.content);
  editor.config.onchange = function (newHtml) {
    emits("change", newHtml);
  };
  // 配置触发 onchange 的时间频率,默认为 200ms
  editor.config.onchangeTimeout = 500; // 修改为 500ms
});
</script>
<style lang="scss" scoped>
#myeditor {
  width: 100%;
}
</style>

首页引用(前端):

<el-form-item label="内容" prop="content">
<Editor @change="handleChange" :content="newsForm.content" v-if="newsForm.content"/>
</el-form-item>
// 富文本编辑器内容改变的回调
const handleChange = (data) => {
  newsForm.content = data;
};

后端直接分享NewsService.addList:

  addList: async ({
    title,
    content,
    category,
    cover,
    isPublish,
    created_at,
    userId,
  }) => {
    return new Promise((resolve, reject) => {
      const sql =
        "INSERT INTO news (title, content, category, cover, isPublish, created_at, userId) VALUES (?, ?, ?, ?, ?, ?, ?)";
      connection.query(
        sql,
        [title, content, category, cover, isPublish, created_at, userId],
        (err, results) => {
          if (err) {
            console.log("添加新闻信息错误:", err);
            reject(err);
          } else {
            resolve(results);
          }
        }
      );
    });
  },

3.2、新闻列表(/adminapi/news/list)

前端传递当前用户id、类别id、分页参数,后端按照关键词返回。这里后端的优化就是在返回列表时,富文本内容过大,先不返回,等编辑或者预览,通过详情接口,查询全部数据。

3.2.1、分页实现

前端借助分页器

const currentPage = ref(1);
const pageSize = ref(5);
const total = ref(0);
const handleSizeChange = (val) => {
  pageSize.value = val;
  currentPage.value = 1; // 切换每页条数时重置到第一页
  getData();
};
const handleCurrentChange = (val) => {
  currentPage.value = val;
  getData();
};
const getData = async () => {
  const params = {
    currentPage: currentPage.value,
    pageSize: pageSize.value,
    category: searchForm.category,
    userId:
      store.state.userInfo.role === 2 ? store.state.userInfo.id : undefined, //管理员查看所有,自己查看自己
  };
  const res = await axios.get("/adminapi/news/list", { params });
  if (res.code == 200) {
    tableData.value = res.data;
    total.value = res.total;
  } else {
    ElMessage.error(res.message);
  }
};

后端核心代码:

  getList: async (page = 1, pageSize = 10, category = "", userId = null) => {
    return new Promise((resolve, reject) => {
      let sql = "SELECT * FROM news";
      let params = [];
      let whereArr = [];

      if (category) {
        whereArr.push("category = ?");
        params.push(category);
      }
      if (userId) {
        whereArr.push("userId = ?");
        params.push(userId);
      }
      if (whereArr.length > 0) {
        sql += " WHERE " + whereArr.join(" AND ");// SELECT * FROM news WHERE category = ? AND userId = ?
      }
      // 添加排序和分页
      sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
      const offset = (page - 1) * pageSize;
      params.push(pageSize, offset);

      connection.query(sql, params, (err, results) => {
        if (err) {
          console.log("查询错误:", err);
          reject(err);
        } else {
          console.log("查询结果:", results);
          resolve(results);
        }
      });
    });
  },
  getTotal: async (category = "", userId = null) => {
    return new Promise((resolve, reject) => {
      let sql = "SELECT COUNT(*) as total FROM news";
      let params = [];
      let whereArr = [];

      if (category) {
        whereArr.push("category = ?");
        params.push(category);
      }
      if (userId) {
        whereArr.push("userId = ?");
        params.push(userId);
      }
      if (whereArr.length > 0) {
        sql += " WHERE " + whereArr.join(" AND ");
      }

      connection.query(sql, params, (err, results) => {
        if (err) {
          console.log("查询总数错误:", err);
          reject(err);
        } else {
          resolve(results[0].total);
        }
      });
    });
  },

3.2.2、时间渲染

渲染更新时间:Moment.js 中文网

npm install moment --save 

        <el-table-column label="更新时间" width="180" align="center">
          <template #default="{ row }">
            <span>{{ formatTime.getTime(row.edit_time) }}</span>
          </template>
        </el-table-column>

utils文件夹里的formatTime.js代码

import moment from 'moment'
moment.locale('zh-cn')
const formatTime={
  getTime(time) {
    return moment(time).format('YYYY-MM-DD HH:mm:ss')
  },
}
export default formatTime

3.3、发布新闻(/adminapi/news/publish)

点击前端表格该列的“是否发布”按钮,传递id和自身状态,后端接收后,加上edit_time: new Date(),记录更新时间,改变sql该条数据后,返回改变后的数据列表。

        <el-table-column label="是否发布" width="180" align="center">
          <template #default="{ row }">
            <!-- 用返回的数字来控制 switch 的值 -->
            <el-switch
              :active-value="1"
              :inactive-value="0"
              v-model="row.isPublish"
              @change="row.isPublish ? changeSwitch(row) : ''"
            ></el-switch>
          </template>
        </el-table-column>
const changeSwitch = async (row) => {
  const res = await axios.post("/adminapi/news/publish", {
    id: row.id,
    isPublish: row.isPublish,
  });
  if (res.code === 200) {
    ElMessage.success(res.message);
    getData();
  } else {
    ElMessage.error(res.message);
  }
};

后端核心代码:

  publishList: async ({ id, isPublish, edit_time }) => {
    return new Promise((resolve, reject) => {
      // sql语句里的参数顺序最好和传递给数据库的参数顺序一致
      let sql = "UPDATE news SET isPublish = ?,edit_time = ? WHERE id = ?";
      connection.query(sql, [isPublish, edit_time, id], (err, results) => {
        if (err) {
          console.log("发布新闻信息错误:", err);
          reject(err);
        } else {
          resolve(results);
        }
      });
    });
  },

3.4、预览新闻(/adminapi/news/detail/:id)

点击预览,调用详情接口,用v-html回显富文本内容。

::v-deep .news-content {
  img {
    max-width: 100%;
  }
}
    <el-dialog v-model="previewFormVisible" title="预览新闻" width="70%">
      <div>
        <h2>
          {{ previewInfo.title }}
        </h2>
        <p>更新时间:{{ formatTime.getTime(previewInfo.edit_time) }}</p>
        <el-divider>
          <el-icon><star-filled /></el-icon>
        </el-divider>
        <!-- 解析富文本代码 :v-html="previewInfo.content"-->
        <div class="news-content" v-html="previewInfo.content"></div>
      </div>
    </el-dialog>

后端核心代码

  getById: async (id) => {
    return new Promise((resolve, reject) => {
      const sql = "SELECT * FROM news WHERE id = ?";
      connection.query(sql, [id], (err, results) => {
        if (err) {
          console.log("根据ID查询新闻错误:", err);
          reject(err);
        } else {
          resolve(results[0] || null);
        }
      });
    });
  },

3.5、删除新闻(/adminapi/news/delete/:id)

逻辑参考用户删除

3.7、编辑新闻(/adminapi/news/detail/:id)

编辑新闻放在一个新的页面(新建路由:/news-manage/newsedit/:id),点击编辑按钮,跳转到详情页面,初始化时进行页面回显,提交后返回列表页

后端核心代码:

  updateList: async ({
    id,
    title,
    content,
    category,
    cover,
    isPublish,
    edit_time,
  }) => {
    return new Promise((resolve, reject) => {
      let sql =
        "UPDATE news SET title = ?, content = ?, category = ?, isPublish = ?, edit_time = ?";
      let params = [title, content, category, isPublish, edit_time];

      // 如果提供了cover字段,则更新cover
      if (cover !== undefined) {
        sql =
          "UPDATE news SET title = ?, content = ?, category = ?, cover = ?, isPublish = ?, edit_time = ?";
        params = [title, content, category, cover, isPublish, edit_time];
      }

      sql += " WHERE id = ?";
      params.push(id);

      connection.query(sql, params, (err, results) => {
        if (err) {
          console.log("修改新闻信息错误:", err);
          reject(err);
        } else {
          resolve(results);
        }
      });
    });
  },

最后,这个项目预计分为两页讲解,后续会上传前后端所有代码,在gitCode可以自行下载,快速查看后端逻辑,可通过子目录的接口进行搜索。


网站公告

今日签到

点亮在社区的每一天
去签到