springboot + Vue前后端项目(第十八记)

news2024/10/7 6:56:27

项目实战第十八记

  • 写在前面
  • 1. 前台页面搭建(Front.vue)
  • 2. 路由
  • 3.改动登录页面Login.vue
  • 4. 前台主页面搭建Home.vue
  • 总结
  • 写在最后

写在前面

本篇主要讲解系统前台搭建,通常较大的项目都会搭建前台

1. 普通用户登录成功后前台页面效果(缺少后台管理导航栏)
在这里插入图片描述
2. 管理员登录成功后前台页面效果
在这里插入图片描述

1. 前台页面搭建(Front.vue)

<template>
  <div>
<!--    头部-->
    <div style="display: flex; height: 60px; line-height: 60px; border-bottom: 1px solid #eee">
      <div style="width: 300px; display: flex; padding-left: 30px">
        <div style="width: 60px">
          <img src="../../assets/img/logo1.png" alt="" style="width: 50px; position: relative; top: 5px; right: 0">
        </div>
        <div style="flex: 1">欢迎来到吉吉系统</div>
      </div>
      <div style="flex: 1">
        <el-menu default-active="1" class="el-menu-demo" mode="horizontal" router>
          <el-menu-item index="/front/home">首页</el-menu-item>
          <el-menu-item index="/front/video">视频播放</el-menu-item>
          <el-menu-item index="*">文章列表</el-menu-item>
          <el-menu-item index="/" v-if="user.role === 'ADMIN'">后台管理</el-menu-item>
          <el-submenu index="2">
            <template slot="title">我的工作台</template>
            <el-menu-item index="/front/item1">选项1</el-menu-item>
            <el-menu-item index="2-2">选项2</el-menu-item>
            <el-menu-item index="2-3">选项3</el-menu-item>
            <el-submenu index="2-4">
              <template slot="title">选项4</template>
              <el-menu-item index="2-4-1">选项1</el-menu-item>
              <el-menu-item index="2-4-2">选项2</el-menu-item>
              <el-menu-item index="2-4-3">选项3</el-menu-item>
            </el-submenu>
          </el-submenu>
          <el-menu-item index="4"><a href="https://www.ele.me" target="_blank">订单管理</a></el-menu-item>
        </el-menu>
      </div>
      <div style="width: 200px">
        <div v-if="!user.username" style="text-align: right; padding-right: 30px">
          <el-button @click="$router.push('/login')">登录</el-button>
          <el-button @click="$router.push('/register')">注册</el-button>
        </div>
        <div v-else>
          <el-dropdown style="width: 150px; cursor: pointer; text-align: right">
            <div style="display: inline-block">
              <img :src="user.avatarUrl" alt=""
                   style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px">
              <span>{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
            </div>
            <el-dropdown-menu slot="dropdown" style="width: 100px; text-align: center">
              <el-dropdown-item style="font-size: 14px; padding: 5px 0">
                <router-link to="/front/password">修改密码</router-link>
              </el-dropdown-item>
              <el-dropdown-item style="font-size: 14px; padding: 5px 0">
                <router-link to="/front/person">个人信息</router-link>
              </el-dropdown-item>
              <el-dropdown-item style="font-size: 14px; padding: 5px 0">
                <span style="text-decoration: none" @click="logout">退出</span>
              </el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
        </div>
      </div>
    </div>

    <div style="width: 1000px; margin: 0 auto">
      <router-view @refreshUser="getUser" />
    </div>
  </div>
</template>

<script>
export default {
  name: "Front",
  data() {
    return {
      user: localStorage.getItem("loginUser") ? JSON.parse(localStorage.getItem("loginUser")) : {}
    }
  },
  created() {

  },
  methods: {
    logout() {
      this.$store.commit("logout")
      this.$message.success("退出成功")
    },
    getUser() {
      let username = localStorage.getItem("loginUser") ? JSON.parse(localStorage.getItem("loginUser")).username : ""
      if (username) {
        // 从后台获取User数据
        this.request.get("/user/username/" + username).then(res => {
          // 重新赋值后台的最新User数据
          this.user = res.data
        })
      }
    }
  }
}
</script>

<style>
.item{
  display: inline-block;
  width: 100px;

  text-align: center;
  cursor: pointer
}
.item a {
  color: 	#1E90FF;
}
.item:hover{
  background-color: 	LightPink;
}
</style>

2. 路由

{
    path: '/front',
    name: '前台首页',
    component: () => import('../views/front/Front.vue'),
    children: [
      {
        path: 'home',
        name: '前台主页',
        component: () => import('../views/front/Home.vue')
      },
      {
        path: 'password',
        name: '修改密码',
        component: () => import('../views/front/Password.vue')
      },
      {
        path: 'person',
        name: '个人信息',
        component: () => import('../views/front/Person.vue')
      },
    ]
  }

完整的代码如下:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Manage from '../views/Manage.vue'
import store from "@/store";

Vue.use(VueRouter)
//定义一个路由对象数组
const routes = [
  {
    path: '/login',
    name: '登录',
    component: () => import('../views/Login.vue')
  },
  {
    path: '/register',
    name: '注册',
    component: () => import('../views/Register.vue')
  },
  {
    path: '/404',
    name: '404',
    component: () => import('../views/404.vue')
  },
  {
    path: '/front',
    name: '前台首页',
    component: () => import('../views/front/Front.vue'),
    children: [
      {
        path: 'home',
        name: '前台主页',
        component: () => import('../views/front/Home.vue')
      },
      {
        path: 'password',
        name: '修改密码',
        component: () => import('../views/front/Password.vue')
      },
      {
        path: 'person',
        name: '个人信息',
        component: () => import('../views/front/Person.vue')
      },
      {
        path: 'video',
        name: '视频播放',
        component: () => import('../views/front/Video.vue')
      },
    ]
  }
]

//使用路由对象数组创建路由实例,供main.js引用
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

// 注意:刷新页面会导致页面路由重置
export const setRoutes = () => {
  const storeMenus = localStorage.getItem("menus");
  if (storeMenus) {

    // 获取当前的路由对象名称数组
    const currentRouteNames = router.getRoutes().map(v => v.name)
    if (!currentRouteNames.includes('Manage')) {
      // 拼装动态路由
      const manageRoute = { path: '/', name: 'Manage', component: () => import('../views/Manage.vue'), redirect: "/home", children: [
          { path: 'person', name: '个人信息', component: () => import('../views/Person.vue'),meta: { title: '个人信息' }},
          { path: 'password', name: '修改密码', component: () => import('../views/Password.vue'),meta: { title: '修改密码' }}
        ] }
      const menus = JSON.parse(storeMenus)
      menus.forEach(item => {
        if (item.path) {  // 当且仅当path不为空的时候才去设置路由
          let itemMenu = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.pagePath + '.vue'),meta: { title: item.name }}
          manageRoute.children.push(itemMenu)
        } else if(item.children.length) {
          item.children.forEach(item => {
            if (item.path) {
              let itemMenu = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.pagePath + '.vue'),meta: { title: item.name }}
              manageRoute.children.push(itemMenu)
            }
          })
        }
      })
      // 动态添加到现在的路由对象中去
      router.addRoute(manageRoute)
    }

  }
}

// 重置我就再set一次路由
setRoutes()


// 路由守卫
router.beforeEach((to, from, next) => {
  // localStorage.setItem('currentPathName',to.name);   // 设置当前的路由名称,为了在Header组件中去使用
  // store.commit('setPath')    // 触发store的数据更新


  // 未找到路由情况
  if(!to.matched.length){
    const storeMenus = localStorage.getItem("menus");
    if(storeMenus){   // 有菜单没有找到路由,跳转至 404页面
      next("/404")
    }else {    // // 没有菜单,直接跳转至登录页
      next("/login")
    }
  }

  next()   // 放行路由
})

export default router

3.改动登录页面Login.vue

**逻辑:**登录成功后进入前台首页,而不是进入后台;后台只有管理员能进入,普通用户提供进入前台,但是不提供后台管理导航栏选项;

login() {
      this.$refs['userForm'].validate((valid) => {
        if (valid) {  // 表单校验合法
          this.request.post("/user/login", this.user).then(res => {
            if(res.code === 200 || res.code === '200') {
              localStorage.setItem('loginUser',JSON.stringify(res.data));
              localStorage.setItem("menus",JSON.stringify(res.data.menus));
              setRoutes()
              this.$router.push("/front/home")
              this.$message.success("登录成功")
            } else {
              this.$message.error(res.msg)
            }
          })
        } else {
          return false;
        }
      });
    }

4. 前台主页面搭建Home.vue

<template>
  <div>
    <div style="margin: 10px 0">
      <el-carousel height="450px" :interval="10000">
        <el-carousel-item v-for="item in imgs" :key="item">
          <img :src="item" alt="" style="width: 100%">
        </el-carousel-item>
      </el-carousel>
    </div>

    <div style="margin: 10px 0">
      <el-row :gutter="10">
        <el-col :span="6" v-for="item in files" :key="item.id" style="margin-bottom: 10px">

          <div style="border: 1px solid #ccc; padding-bottom: 10px">
            <img :src="item.url" alt="" style="width: 100%;height: 100px;">
            <div style="color: #666; padding: 10px">{{ item.name }}</div>
            <div style="padding: 10px"><el-button type="primary">购买</el-button></div>
          </div>
        </el-col>
      </el-row>
    </div>

    <div>
      <p>&copy; {{ new Date().getFullYear() }} 吉吉网站. All Rights Reserved.</p>
    </div>

  </div>


</template>

<script>
export default {
  name: "FrontHome",
  data() {
    return {
      imgs: [
          'https://img30.360buyimg.com/babel/s1580x830_jfs/t1/109361/24/22897/74054/621ede58E099d37e3/f12730c81df6046a.jpg!cc_1580x830.webp',
          'https://img13.360buyimg.com/babel/s1580x830_jfs/t1/96398/30/23715/70228/6221e9d0Ec1b9fe65/f66e2ad76314d6cd.jpg!cc_1580x830.webp'
      ],
      files: []
    }
  },
  created() {
   this.getImg()
  },
  methods: {
    getImg(){
      this.request.get("/file").then(res => {
        this.files = res.data.filter(v => v.type === 'png' || v.type === 'jpg' || v.type === 'webp')
      })
    }
  }
}
</script>

<style scoped>
p {
  font-size: 16px;
  color: #555;
  text-align: center;
}
</style>

后端查询所有文件的接口FileController

@GetMapping
public Result findAll() {
    return Result.success(fileService.list());
}

完整代码:

package com.ppj.controller;


import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ppj.entity.Files;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ppj.common.Result;

import com.ppj.service.IFileService;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author ppj
 * @since 2024-05-21
 */
@RestController
@RequestMapping("/file")
public class FileController {

    @Resource
    private IFileService fileService;

    @Value("${files.upload.path}")
    private String fileUploadPath;

    // 新增或者更新
    @PostMapping
    public Result save(@RequestBody Files file) {
        fileService.saveOrUpdate(file);
        return Result.success();
    }

    @DeleteMapping("/{fileIds}")
    public Result delete(@PathVariable Integer[] fileIds) {
        fileService.removeByIds(Arrays.asList(fileIds));
        return Result.success();
    }

    @GetMapping
    public Result findAll() {
        return Result.success(fileService.list());
    }


    @GetMapping("/page")
    public Result findPage(@RequestParam Integer pageNum,
                                @RequestParam Integer pageSize,
                           @RequestParam String name) {
        QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
        queryWrapper.like("name",name);
//        queryWrapper.orderByDesc("id");
        return Result.success(fileService.page(new Page<>(pageNum, pageSize), queryWrapper));
    }


    /**
     * 文件上传接口
     * @param file 前端传递过来的文件
     * @return
     * @throws IOException
     */
    @PostMapping("/upload")
    public String upload(@RequestParam MultipartFile file) throws IOException {
        String originalFilename = file.getOriginalFilename();
        String type = FileUtil.extName(originalFilename);
        long size = file.getSize();

        // 定义一个文件唯一的标识码
        String uuid = IdUtil.fastSimpleUUID();
        String fileUUID = uuid + StrUtil.DOT + type;

        File uploadFile = new File(fileUploadPath + fileUUID);
        // 判断配置的文件目录是否存在,若不存在则创建一个新的文件目录
        File parentFile = uploadFile.getParentFile();
        if(!parentFile.exists()) {
            parentFile.mkdirs();
        }

        String url;
        // 获取文件的md5
        String md5 = SecureUtil.md5(file.getInputStream());
        // 从数据库查询是否存在相同的记录
        Files dbFiles = getFileByMd5(md5);
        if (dbFiles != null) { // 文件已存在,直接返回数据库里的url
            url = dbFiles.getUrl();
        } else {  // 文件不存在才生成url,保存数据至数据库
            // 上传文件到磁盘
            file.transferTo(uploadFile);
            // 数据库若不存在重复文件,则不删除刚才上传的文件
            url = "http://localhost:9000/file/" + fileUUID;
            // 存储数据库
            Files saveFile = new Files();
            saveFile.setName(originalFilename);
            saveFile.setType(type);
            saveFile.setSize(size/1024);
            saveFile.setUrl(url);
            saveFile.setMd5(md5);
            fileService.saveOrUpdate(saveFile);
        }
        return url;
    }

    /**
     * 通过文件的md5查询文件
     * @param md5
     * @return
     */
    private Files getFileByMd5(String md5) {
        // 查询文件的md5是否存在
        QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("md5", md5);
        Files one = fileService.getOne(queryWrapper);
        return one != null ? one : null;
    }

    /**
     * 文件下载接口   http://localhost:9090/file/{fileUUID}
     * @param fileUUID
     * @param response
     * @throws IOException
     */
    @GetMapping("/{fileUUID}")
    public void download(@PathVariable String fileUUID, HttpServletResponse response) throws IOException {
        // 根据文件的唯一标识码获取文件
        File uploadFile = new File(fileUploadPath + fileUUID);
        // 设置输出流的格式
        ServletOutputStream os = response.getOutputStream();
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileUUID, "UTF-8"));
        response.setContentType("application/octet-stream");

        // 读取文件的字节流
        os.write(FileUtil.readBytes(uploadFile));
        os.flush();
        os.close();
    }

    @PostMapping("/update")
    public Result changeEnable(@RequestBody Files files){
        return fileService.saveOrUpdate(files)?Result.success():Result.error();
    }

}

总结

  • 本篇主要的难点是前台页面的设计与实现

写在最后

如果此文对您有所帮助,请帅戈靓女们务必不要吝啬你们的Zan,感谢!!不懂的可以在评论区评论,有空会及时回复。
文章会一直更新

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1854688.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flutter 如何发布安卓应用?

android:hardwareAccelerated“true” android:windowSoftInputMode“adjustResize”> <meta-data android:name“flutterEmbedding” android:value“2” /> Flutter生成的文件建议是大部分内容可以保留不动&#xff0c;但是可以根据需要进行修改。 具体可能要修…

一款有趣的Python库绘制风向图,小白容易上手

利用 Python 绘制风向图 绘制风向图通常使用 matplotlib 库的 Barbs 类来实现.这个类用于绘制风向和风速的矢量场,可以实现不同的风向图风格. 安装 ## 命令安装 matplotlib 库&#xff1a;pip install matplotlib用法 下面是一个简单的示例代码,绘制风向图&#xff1a; 使…

分布式,容错:10台电脑坏了2台

由10台电脑组成的分布式系统&#xff0c;随机、任意坏了2台&#xff0c;剩下的8台电脑仍然储存着全部信息&#xff0c;可以继续服务。这是怎么做到的&#xff1f; 设N台电脑&#xff0c;坏了H台&#xff0c;要保证上述性质&#xff0c;需要有冗余&#xff0c;总的存储量降低为…

路由

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 客户端&#xff08;例如浏览器&#xff09;把请求发送给 Web 服务器&#xff0c;Web 服务器再把请求发送给 Flask程序实例。程序实例需要知道对每个U…

图像超分辨率重建

一、什么是图像超分辨 图像超分辨是一种技术&#xff0c;旨在通过硬件或软件的方法提高原有图像的分辨率。这一过程涉及从一系列低分辨率的图像中获取一幅高分辨率的图像&#xff0c;实现了时间分辨率向空间分辨率的转换。超分辨率重建的核心思想是利用多帧图像序列的时间带宽来…

压力测试Monkey命令参数和报告分析

目录 常用参数 -p <测试的包名列表> -v 显示日志详细程度 -s 伪随机数生成器的种子值 --throttle < 毫秒> --ignore-crashes 忽略崩溃 --ignore-timeouts 忽略超时 --monitor-native-crashes 监视本地崩溃代码 --ignore-security-exceptions 忽略安全异常 …

【vue3|第13期】深入了解Vue3生命周期:管理组件的诞生、成长与消亡

日期&#xff1a;2024年6月22日 作者&#xff1a;Commas 签名&#xff1a;(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释&#xff1a;如果您觉得有所帮助&#xff0c;帮忙点个赞&#xff0c;也可以关注我&#xff0c;我们一起成长&#xff1b;如果有不对的地方&#xf…

夏季城市内涝防治:视频汇聚系统智能AI技术助力城市自然灾害应急管理

据新闻报道&#xff0c;6月19日至20日&#xff0c;受强降雨影响&#xff0c;广西桂林城区及周边等地出现今年入汛以来持续时间最长、累计降水量最大、影响范围最广、致灾风险最高的暴雨天气过程&#xff0c;导致桂林市区多处发生洪水内涝&#xff0c;房屋被淹、道路受阻、人员被…

SuiNS发布子名及新命名标准,推动Web3身份结构的进步

SuiNS子名是Sui Name Service的强大扩展&#xff0c;最近与新命名标准一起发布。子名允许用户在一个主要的SuiNS名下创建额外的自定义身份&#xff0c;而无需额外费用。用户 gia 可以创建如 gaminggia 或 lendinggia 这样的子名&#xff0c;从而增强个人组织和支持群组与组织的…

Inception_V2_V3

Inception_V2_V3 CNN卷积网络的发展史 1. LetNet5(1998) 2. AlexNet(2012) 3. ZFNet(2013) 4. VGGNet(2014) 5. GoogLeNet(2014) 6. ResNet(2015) 7. DenseNet(2017) 8. EfficientNet(2019) 9. Vision Transformers(2020) 10. 自适应卷积网络(2021) 上面列出了发展到现在CNN的…

GStreamer学习4----写一个插件

参考资料&#xff1a; Constructing the Boilerplate gstreamer插件-CSDN博客 在Constructing the Boilerplate 里面有生成插件的例子&#xff0c; shell $ git clone https://gitlab.freedesktop.org/gstreamer/gst-template.git 使用里面的工具自动生成一个插件程序&…

linux 简单使用 sftp 和 lftp命令

目录 一. 环境准备二. sftp命令连接到SFTP服务器三. lftp命令3.1 连接FTP和SFTP服务器3.2 将文件从sftp服务器下载到本地指定目录 四. 通过WinSCP命令行从SFTP服务器获取文件到Windows 一. 环境准备 ⏹在安卓手机上下载个MiXplorer&#xff0c;用作SFTP和FTP服务器 官网: htt…

Mathtype7在Word2016中闪退(安装过6)

安装教程&#xff1a;https://blog.csdn.net/Little_pudding10/article/details/135465291 Mathtype7在Word2016中闪退是因为安装过Mathtype6&#xff0c;MathPage.wll和MathType Comm***.dotm)&#xff0c;不会随着Mathtype的删除自动删除&#xff0c;而新版的Mathtype中的文件…

Debian Linux安装minikubekubectl

minikube&kubectl minkube用于在本地开发环境中快速搭建一个单节点的Kubernetes集群,还有k3s&#xff0c;k3d&#xff0c;kind都是轻量级的k8skubectl是使用K8s API 与K8s集群的控制面进行通信的命令行工具 这里使用Debian Linux演示&#xff0c;其他系统安装见官网,首先…

React+TS 从零开始教程(2):简中简 HelloWolrd

源码链接&#xff1a;https://pan.quark.cn/s/c6fbc31dcb02 这一节&#xff0c;我们来见识ReactTS的威力&#xff0c;开始上手开发第一个组件&#xff0c;什么组件呢&#xff1f; 当然是简中简的 HelloWolrd组件啦。 在src下创建一个components&#xff0c;然后新建Hello.tsx …

nlp基础-文本预处理及循环神经网络

1 认识文本预处理 1 文本预处理及其作用 定义&#xff1a;文本送给模型之前&#xff0c;提前要做的工作 作用&#xff1a;指导模型超参数的选择 、提升模型的评估指标 举个例子&#xff1a; 思路常识&#xff0c;打造成 X Y关于Y&#xff1a;10分类标签是否均衡关于X&#xf…

【break】大头哥哥做题

【break】大头哥哥做题 时间限制: 1000 ms 内存限制: 65536 KB 【题目描述】 【参考代码】 #include <iostream> using namespace std; int main(){ int sum 0;//求和int day 0;//天数 while(1){int a;cin>>a;if(a-1){break;//结束当前循环 }sum sum a; …

自动更新阿里云CDN SSL证书

deploy-certificate-to-aliyun 随着各大CA机构开始收割用户&#xff0c;云厂商们提供的免费SSL证书也由之前的12个月变成现在的3个月。笔者一直使用阿里云的OSS作为图床&#xff0c;说实话在如果继续在阿里云上三个月免费一换也太频繁了 笔者在这里使用github action来每隔两个…

odoo的采购询价单,默认情况下显示‘draft‘,‘sent‘,‘purchase‘,请问什么情况下才会显示‘to approve‘?

odoo的采购询价单&#xff0c;默认情况下显示’draft’,‘sent’,‘purchase’&#xff0c;请问什么情况下才会显示’to approve’? 见下图&#xff1a; 这与操作人员的角色是相关的&#xff1a; 当操作人员是群组 “采购 / 用户”时&#xff0c;点击“confirm order/确认订…

细说AGV的12种导航方式和原理

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 新书《智能物流系统构成与技术实践》人俱乐部 这十二种导航方式各自具有不同的特点和应用场景&#xff0c;下面我将逐一进行简要介绍&#xff1a; 磁钉导航&#xff1a; 原理&#xf…