目录
2.2、个人中心(/adminapi/user/upload)
2.5、删除用户(/adminapi/user/delete/:id)
2.6、编辑用户(/adminapi/user/update)
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可以自行下载,快速查看后端逻辑,可通过子目录的接口进行搜索。