快速搭建SpringBoot3+Vue3管理系统
前端项目搭建(默认开发环境:node20,Jdk17)
创建项目并下载依赖--执行以下命令
- 创建项目
javascript
yarn create vite admin-vue --template
2.下载依赖(也可按需下载)
javascript
yarn add echarts --save
yarn add vue-router --save
yarn add element-plus --save
yarn add axios --save
yarn add sass--save
3.导入IDEA
4.配置快速启动
也可以在终端中使用命令启动,如下:
5.配置Axios
在src目录下新建axios目录,并创建request.js文件
javascript
import axios from 'axios'
const request = axios.create({
timeout: 20000,
baseURL: 'http://127.0.0.1:8080',
})
export default request
6.配置路由
在src目录下新建axios目录,并创建router.js文件
javascript
import {createRouter,createWebHashHistory} from "vue-router";
// 在router.js文件中创建路由实例对象
const router=createRouter({
history:createWebHashHistory(),
routes:[//路由规则
]
})
// 输出实例对象
export default router;
7.挂载ElementPlus和路由
在main.js中挂载
javascript
import { createApp } from 'vue'
import './style.css'
// 导入element-plu
import ElementPlus from 'element-plus'
// 导入路由
import router from './router/router.js'
// 导入css样式
import 'element-plus/dist/index.css'
import App from './App.vue'
// 挂载element-plu和router
createApp(App).use(ElementPlus).use(router).mount('#app')
8.绝对路径配置
在vite.config.js中配置绝对路径别名--@
javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
// 绝对路径别名
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
8.创建布局组件
8.1 顶部水平导航
html
<script setup>
import {onMounted, ref} from "vue";
import router from "@/router/router.js";
// 图片地址
const url=ref("/element-plus-logo.svg")
// 将用户信息从浏览器缓存中取出
const user=ref(JSON.parse(localStorage.getItem('xm-user') || '{}'))
// 退出登录
const logout=()=>{
localStorage.removeItem('xm-user');
router.push("/login");
}
</script>
<template>
<!-- 顶部导航栏-->
<el-menu
router
class="el-menu-demo"
mode="horizontal"
text-color="grey"
background-color="white"
:ellipsis="false"
>
<el-menu-item index="1">
<el-image style="width: 100px; height: 100px" :src="url" />|商品信息管理系统
</el-menu-item>
<el-menu-item index="/">首页
</el-menu-item>
<el-menu-item index="/TeachingCenter">
<span>教学中心</span>
</el-menu-item>
<el-menu-item index="/TeachingCenter">
<span>教学资源</span>
</el-menu-item>
<el-menu-item index="/Goods">
<span>商品信息管理</span>
</el-menu-item>
<el-menu-item index="/TeachingCenter">
<span>教学实践</span>
</el-menu-item>
<el-menu-item index="/TeachingCenter">
<span>问答社区</span>
</el-menu-item>
<el-sub-menu index="10" v-if="user.id">
<template #title>
<img :src="user.avatar" width="40px" height="40px" style="border-radius: 50%;margin-right: 5px" />
{{ user.name }}
</template>
<el-menu-item index="/manage/personal">个人中心</el-menu-item>
<el-menu-item index="10-2">修改密码</el-menu-item>
<el-menu-item index="10-3" @click="logout">退出登录</el-menu-item>
</el-sub-menu>
<el-menu-item index="/login" v-else>
<el-button type="primary" round>登录</el-button>
</el-menu-item>
</el-menu>
</template>
<style scoped>
.el-menu--horizontal > .el-menu-item:nth-child(1) {
margin-right: auto;
}
</style>
8.2 侧边导航栏
html
<script setup>
import {Goods, House, Promotion, User,Avatar} from "@element-plus/icons-vue";
</script>
<template>
<!-- 侧边导航栏-->
<el-aside width="200px" >
<el-menu default-active="1" style="min-height: 100%;overflow-x: hidden;"
class="el-menu"
background-color="white"
text-color="grey"
router>
<!-- <div style="height: 60px; line-height: 60px; text-align: center;">
<img src="../assets/logo.png" alt="" style="width: 20px;position: relative; top: 5px;margin-right: 5px;">
<b style="color: white;">后台管理系统</b>
</div>-->
<el-menu-item index="/manage/index" >
<el-icon><House /></el-icon>
主页</el-menu-item>
<el-menu-item index="/manage/goods">
<el-icon><Goods /></el-icon>
<span>商品信息管理</span>
</el-menu-item>
<el-sub-menu index="2">
<template #title><el-icon><User /></el-icon>用户管理</template>
<el-menu-item ><el-icon><User /></el-icon>用户信息</el-menu-item>
<el-menu-item index="/manage/admin">
<el-icon><Avatar /></el-icon>
<span>管理员信息</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item ><el-icon><Promotion /></el-icon>订单管理</el-menu-item>
</el-menu>
</el-aside>
</template>
<style scoped>
.el-menu{
height: 100vh;
}
</style>
8.3 底部导航栏
html
<script setup>
</script>
<template>
<p alog-group="copy">©2024 商品信息管理系统
<a rel="noopener" href="//www.baidu.com/duty/">使用必读</a>
<a rel="noopener" href="https://tieba.baidu.com/tb/eula.html">协议</a>
<a href="https://tieba.baidu.com/tb/cms/tieba-fe/tieba_promise.html">隐私政策</a>
<a rel="noopener" href="//tieba.baidu.com/hermes/feedback">投诉反馈</a>
信息网络传播视听节目许可证 0110516 违法和不良信息举报电话:400-921-5119
<a rel="noopener" href="http://net.china.cn/chinese/index.htm" target="_blank">
<img src="//tb2.bdstatic.com/tb/static-common/img/net_7a8d27a.png" width="20"></a>
<a rel="noopener" href="http://www.bj.cyberpolice.cn/index.htm" target="_blank"><img title="首都网络110报警服务" src="//tb2.bdstatic.com/tb/static-common/img/110_97c9232.png" width="20"></a>
</p>
</template>
<style scoped>
</style>
9.创建整体布局,引用子组件
html
<script setup>
// 注册子组件
import Header from "@/components/Header.vue";
import Footer from "@/components/Footer.vue";
import Aside from "@/components/Aside.vue";
import {ref} from "vue";
import {useRouter} from "vue-router";
// 获取全局路由
const router=useRouter();
// 判断是否登录
// 将用户信息从浏览器缓存中取出
const user=ref(JSON.parse(localStorage.getItem('xm-user') || '{}'))
if (user.value.name==null){//如果没有数据
router.push("/login");
}
</script>
<template>
<!-- container布局 -->
<div class="common-layout">
<el-container>
<!-- 顶部导航栏-->
<el-header><Header></Header></el-header>
<el-container>
<!-- 侧边导航栏-->
<el-aside width="200px"><Aside></Aside></el-aside>
<el-container>
<!-- 内容显示区域-->
<el-main>
<!-- 路由视图-->
<router-view></router-view>
</el-main>
<!-- 底部导航栏-->
<el-footer><Footer></Footer></el-footer>
</el-container>
</el-container>
</el-container>
</div>
</template>
<style scoped>
</style>
10.创建商品信息组件:Goods.vue
javascript
<script setup>
import {onMounted, ref} from "vue";
import request from "../../axios/request.js";
import {ElMessage, ElMessageBox} from "element-plus";
import {
Delete,
Edit,
Message,
Search,
Plus,
House,
User,
Promotion,
Goods,
Download,
UploadFilled
} from "@element-plus/icons-vue";
const list = ref([])
// 查询条件
const name = ref("")
// 模态框是否显示
const dialogVisible = ref(false);
// 商品的信息
const goods = ref({
id: "",
name: "",
price: "",
intro: "",
amount: "",
goodsType: "",
imgurl: "",
remark: "未上架"
})
// 表单校验规则
const rules = {
name: [
{required: true, message: "请输入名称", trigger: "blur"}
],
price: [
{required: true, message: "请输入价格", trigger: "blur"}
],
intro: [
{required: true, message: "请输入简介", trigger: "blur"}
],
amount: [
{required: true, message: "请输入库存", trigger: "blur"}
],
goodsType: [
{required: true, message: "请选择类别", trigger: "blur"}
],
remark: [
{required: true, message: "请选择状态", trigger: "blur"}
]
}
// 表单引用
const goodsForm = ref();
// 分页参数
const pageNum = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 保存选中的数据
const multipleSelection = ref([])
// 查询商品信息
const getList = () => {
request({
url: '/goods/page',
method: 'get',
params: {
page: pageNum.value,
limit: pageSize.value,
goodsName: name.value,
}
}).then((res) => {
// console.log(res)
list.value = res.data.data;
total.value = res.data.count;
// name.value=""
})
}
// 改变每页数据量
const handleSizeChange = (pSize) => {
// console.log(pSize+`items per page`)
pageSize.value = pSize;
getList()
}
// 改变当前页
const handleCurrentChange = (pNum) => {
// console.log(`current page: `+pNum)
pageNum.value = pNum;
getList()
}
// 根据id删除
const del = (id) => {
// console.log(id)
ElMessageBox.confirm(
'是否确认删除?',
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
)
.then(() => {
request({
url: '/goods/del',
method: 'get',
params: {id: id}
}).then(res => {
if (res.data.code == 200) {
ElMessage({
type: 'success',
message: '删除成功!',
})
} else {
ElMessage({
type: 'error',
message: '删除失败!',
})
}
getList()
})
})
.catch(() => {
ElMessage({
type: 'warning',
message: '取消删除!',
})
})
}
// 搜索
const search = () => {
getList()
}
// 新增
const add = () => {
dialogVisible.value = true
}
// 数据提交
const save = () => {
// 校验表单是否合法
goodsForm.value.validate((flag, err) => {
if (flag) {//验证通过
request({
url: '/goods/save',
method: 'post',
data: goods.value
}).then(res => {
getList()
goods.value = ""
dialogVisible.value = false
ElMessage({
type: 'success',
message: '保存成功',
})
})
} else {
ElMessage({
type: 'error',
message: '验证没通过!',
})
}
})
}
// 修改
const edit = (item) => {
// 深拷贝
const newObj = Object.assign({}, item)
dialogVisible.value = true
goods.value = newObj
}
// 获取已选择的数据
const handleSelectionChange = (val) => {
// console.log(val)
multipleSelection.value = val;
}
// 批量删除
const delBatch = () => {
// 获取需要删除的数据的id--数组
let ids = multipleSelection.value.map(item => item.id);
// console.log("ids",ids)
if (ids.length != 0) {
ElMessageBox.confirm(
'是否确认删除?',
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
).then(() => {
request({
url: '/goods/delBatch',
method: 'post',
data: ids
}).then(res => {
ElMessage({
type: 'success',
message: '删除成功!',
})
getList()
// console.log(res)
})
}).catch(() => {
ElMessage({
type: 'warning',
message: '取消删除!',
})
})
} else {
ElMessage({
type: 'error',
message: '请先选择数据!'
})
}
}
// 导入成功后重新加载数据
const handleExcelImportSuccess = () => {
getList();
ElMessage({
type: 'success',
message: '上传成功!',
})
}
// 导出数据
const exp = () => {
window.open(`http://192.168.31.242:8080/goods/export`)
}
// excle导入前验证-->文件类别、文件大小
const cheackExcle = (rawFile) => {
// console.log(rawFile.size)
// 获取文件后缀
let testmsg = rawFile.name.substring(rawFile.name.lastIndexOf('.') + 1)
if (testmsg !== 'xlsx') {
ElMessage.error('只能上传后缀为.xlsx的excle文件')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('文件大小不能超过2M')
return false
}
return true
}
// 图片上传之前的钩子--》判断文件是否是图片、图片大小是否符合要求
const beforeAvatarUpload = (rawFile) => {
console.log("类型:", rawFile.type)
console.log(rawFile.type !== 'image/png');
// 获取文件后缀
let testmsg = rawFile.name.substring(rawFile.name.lastIndexOf('.') + 1)
console.log("后缀:", testmsg)
// 判断文件类型
const extension = testmsg === 'jpg' || testmsg === 'png' || testmsg === 'jpeg' || testmsg === 'gif'
if (!extension) {//不符合要求
ElMessage.error('图片必须是jpg/jpeg/png格式!')
return false
}
// 校验文件大小
if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('图片大小不能超过2MB!')
return false
}
return true
}
// 图片上传后的钩子函数-->回写图片地址
const handleAvatarSuccess = (res) => {
console.log(res)
goods.value.imgurl = res.msg
}
//生命周期函数
onMounted(() => {
getList();
})
</script>
<template>
<!-- 数据表格-->
<el-form-item label="名称" style="margin-top: 20px">
<el-input v-model="name" style="width: 200px" placeholder="请输入商品名称"></el-input>
<el-button type="primary" :icon="Search" style="margin-left: 10px" @click="search">搜索</el-button>
<el-button type="primary" :icon="Plus" style="margin-left: 10px" @click="add">新增</el-button>
<el-button type="danger" :icon="Delete" style="margin-left: 10px" @click="delBatch">批量删除</el-button>
<el-upload :action="'http://' + '192.168.31.242:8080/goods/import'"
:show-file-list="false"
accept="xlsx"
:before-upload="cheackExcle"
:on-success="handleExcelImportSuccess"
style="display: inline-block">
<el-button type="primary" style="margin-left: 10px" :icon="UploadFilled">导入</el-button>
</el-upload>
<el-button type="warning" style="margin-left: 10px" :icon="Download" @click="exp">导出</el-button>
</el-form-item>
<el-table :data="list" width="100%" stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"/>
<el-table-column type="index" label="序号" width="80" :index="1"></el-table-column>
<!-- <el-table-column prop="id" label="ID"></el-table-column>-->
<el-table-column prop="name" label="商品名称"></el-table-column>
<el-table-column prop="price" label="单价"></el-table-column>
<el-table-column prop="intro" label="简介" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="amount" label="库存"></el-table-column>
<el-table-column prop="goodsType" label="类别"></el-table-column>
<el-table-column prop="remark" label="状态"></el-table-column>
<el-table-column label="图片" width="120">
<template #default="scope">
<el-image style="width: 100px; height: 80px; padding: 5px;" :src="scope.row.imgurl" :zoom-rate="1.2"
:max-scale="7" :min-scale="0.2" :preview-src-list="[scope.row.imgurl]" :preview-teleported="true"/>
</template>
</el-table-column>
<el-table-column label="操作" width="300" fixed="right">
<template #default="scope">
<el-button type="primary" :icon="Edit" @click="edit(scope.row)">修改</el-button>
<el-button type="danger" :icon="Delete" @click="del(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 数据分页-->
<div style="padding: 10px">
<el-pagination
v-model:current-page="pageNum"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
size="large"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!--模态框-->
<el-dialog
v-model="dialogVisible"
:title="goods.id ? '修改商品' : '新增商品'"
width="500"
ref="goodsForm"
>
<el-form :model="goods" :rules="rules" ref="goodsForm">
<el-form-item label="商品名称" prop="name">
<el-input v-model="goods.name" autocomplete="off"/>
</el-form-item>
<el-form-item label="商品单价" prop="price">
<el-input v-model="goods.price" autocomplete="off"/>
</el-form-item>
<el-form-item label="商品简介" prop="intro">
<el-input v-model="goods.intro" autocomplete="off"/>
</el-form-item>
<el-form-item label="商品库存" prop="amount">
<el-input v-model="goods.amount" autocomplete="off"/>
</el-form-item>
<el-form-item label="商品类别" prop="goodsType">
<el-select v-model="goods.goodsType" placeholder="请选择商品类别">
<el-option label="手机" value="手机"/>
<el-option label="电脑" value="电脑"/>
<el-option label="水果" value="水果"/>
</el-select>
</el-form-item>
<!-- 单选框-->
<el-form-item label="状态" prop="remark">
<el-radio-group v-model="goods.remark">
<el-radio value="上架">上架</el-radio>
<el-radio value="未上架">未上架</el-radio>
</el-radio-group>
</el-form-item>
<!-- 图片上传-->
<el-form-item label="商品图片">
<el-upload
class="avatar-uploader"
:action="'http://' + '192.168.31.242:8080/files/upload'"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="goods.imgurl" :src="goods.imgurl" class="avatar" style="width: 300px"/>
<el-icon v-else class="avatar-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">
提交
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>
11.创建首页组件:Index.vue
javascript
<script setup>
import { reactive, onMounted,ref} from 'vue'
import { Memo } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
import request from "@/axios/request.js";
// 格式化日期时间
const formattedDate=()=>{
const date = new Date();
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hour = date.getHours().toString().padStart(2, '0');
const minute = date.getMinutes().toString().padStart(2, '0');
const second = date.getSeconds().toString().padStart(2, '0');
const formattedDate = `${year}-${month}-${day} ${hour}:${minute}:${second}`;
console.log(formattedDate);
return formattedDate;
}
let date=formattedDate();
const loginInfo=ref({
loginTime: date,
loginPlace: "云南-昆明"
})
const data = reactive({
user: JSON.parse(localStorage.getItem('xm-user') || '{}'),
noticeData: []
})
// 查询公告信息
const getNoticeList = () => {
request({
url: '/notice/page',
method: 'get',
params: {
page: 1,
limit: 4,
}
}).then((res) => {
// console.log(res)
data.noticeData = res.data.data;
})
}
// 商品数量
const total=ref(0)
// 将用户信息从浏览器缓存中取出
const user=ref(JSON.parse(localStorage.getItem('xm-user') || '{}'))
// 查询商品信息
const getList = () => {
request({
url: '/goods/page',
method: 'get',
params: {
page: 1,
limit: 10,
goodsName: name.value,
}
}).then((res) => {
// console.log(res)
// list.value = res.data.data;
total.value = res.data.count;
console.log(total.value)
// name.value=""
})
}
// 图表1:月度销售额
const initCharts1 = () => {
const myChart = echarts.init(document.getElementById('salesVolume'))
myChart.setOption({
color: ['#1493fa'],
title: { text: '2024年月度销售额' },
xAxis: {
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
name: '月份',
axisLabel: {
interval: 0
},
},
yAxis: {
name: '单位(千万元)',
},
grid: {
left: '3%',
right: '8%',
bottom: '5%',
containLabel: true,
},
legend: {},
series: [
{
data: [6, 7, 8.5, 8, 9, 10, 13, 12, 10, 16, 15, 14],
type: 'line',
name: '销售额',
smooth: true,
label: {
show: true,
position: 'top',
}
}
]
})
// 图表自适应大小
window.onresize = () => {
myChart.resize()
}
}
// 图表2:2024年订单数量
const initCharts2 = () => {
const myChart = echarts.init(document.getElementById('orderQuantity'))
myChart.setOption({
title: { text: '2024年订单数量' },
color: ['#1493fa'],
grid: {
left: '3%',
right: '8%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
name: '月份',
// 类目轴中在 boundaryGap 为 true 的时候有效,可以保证刻度线和标签对齐
axisTick: {
alignWithLabel: true,
},
axisLabel: {
interval: 0,rotate: 45 // 设置刻度标签旋转角度为45度
},
},
legend: {},
yAxis: {
name: '单位(个)',
},
series: [
{
data: [400, 450, 300, 230, 250, 300, 400, 350, 160, 350, 380, 400],
type: 'bar',
barWidth: '60%',
name: '订单总数',
label: {
show: true,
position: 'top',
}
}
]
})
// 图表自适应大小
window.onresize = () => {
myChart.resize()
}
}
//生命周期函数
onMounted(() => {
getList();
getNoticeList()
// 初始化图表
initCharts1()
initCharts2()
})
</script>
<template>
<el-row :gutter="20">
<!-- 登录信息-->
<el-col :span="6">
<el-card class="box-card">
<!-- <el-card class="box-card">
<template #header>
<div class="card-header">
<img :src="user.avatar" style="width: 40px; height: 40px;">
<!– <el-avatar class="avatar" src="@/assets/img.png" shape="square" :size="40"> </el-avatar>–>
<span style="font-size: 24px;">{{ user.name }} </span>
</div>
</template>
<div class="info">
<p>登录时间:{{ loginInfo.loginTime }}</p>
<p>登录地点:{{ loginInfo.loginPlace }}</p>
</div>
</el-card>-->
<div style="display: flex">
<div class="card" style="flex: 50%; height: 400px;width: 200px">
<div style="font-weight: bold; font-size: 18px; padding: 10px 0 30px 10px">系统公告</div>
<el-timeline style="max-width: 600px">
<el-timeline-item
v-for="(item, index) in data.noticeData"
:key="index"
:timestamp="item.time"
>
{{ item.title }}
</el-timeline-item>
</el-timeline>
</div>
<div style="flex: 50%"></div>
</div>
</el-card>
</el-col>
<!-- 单月统计信息展示 -->
<el-col :span="18" >
<el-card class="box-card">
<template #header>
<div class="card-header">
6月统计信息
</div>
</template>
<div class="info" style="height: 340px">
<el-row :gutter="24">
<!-- 商品数量 -->
<el-col :span="8">
<div class="card-container">
<div class="card-left-container" style="background-color: #EEAD0E;">
<el-icon :size="90">
<Memo />
</el-icon>
</div>
<div class="card-right-container">
<p class="number">{{ total }}</p>
<p>商品数量(个)</p>
</div>
</div>
</el-col>
<!-- 商品分类数量 -->
<el-col :span="8">
<div class="card-container">
<div class="card-left-container" style="background-color: #AB82FF;">
<el-icon :size="90">
<Memo />
</el-icon>
</div>
<div class="card-right-container">
<p class="number">20</p>
<p>商品分类数量(个)</p>
</div>
</div>
</el-col>
<!-- 用户访问次数 -->
<el-col :span="8">
<div class="card-container">
<div class="card-left-container" style=" background-color: #63B8FF;">
<el-icon :size="90">
<Memo />
</el-icon>
</div>
<div class="card-right-container">
<p class="number">121</p>
<p>用户访问次数(次)</p>
</div>
</div>
</el-col>
</el-row>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20" style="margin-top: 10px">
<el-col :span="12">
<!-- 通过折线图展示2024年月度销售额 -->
<el-card class="box-card">
<div id="salesVolume" style="width: auto; height:400px;">
</div>
</el-card>
</el-col>
<el-col :span="12">
<!-- 通过柱状图展示2024年订单数量 -->
<el-card class="box-card">
<div id="orderQuantity" style="width: auto; height:400px;">
</div>
</el-card>
</el-col>
</el-row>
</template>
<style scoped>
</style>
12.创建登录组件:Login.vue
javascript
<script setup>
import {ref} from "vue";
import {User,Lock} from "@element-plus/icons-vue";
import request from "@/axios/request.js";
import router from "@/router/router.js";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";
// const router=useRouter()
const user=ref({
username:"",
password:"",
role:""
})
const rules={
username:[
{required:true,message:"请输入用户名",trigger:"blur"}
],
password:[
{required:true,message:"请输入密码",trigger:"blur"}
],
role:[
{required:true,message:"请选择角色",trigger:"blur"}
]
}
// 表单引用
const formRef=ref()
// 登录验证
const login=()=>{
formRef.value.validate((flag)=>{
if(flag){
request({
url:"/login",
method:"post",
data:user.value
}).then(res => {
console.log(res)
if (res.data.code==200){
ElMessage.success("登录成功!")
// 将用户信息保存到浏览器缓存中
localStorage.setItem('xm-user', JSON.stringify(res.data.data))
// 跳转首页
router.push("/manage/index")
}else {
ElMessage.error(res.data.msg)
}
})
}
})
}
</script>
<template>
<div class="login-container">
<div class="login-box">
<div style="font-weight: bold; font-size: 24px; text-align: center; margin-bottom: 30px; color: #1450aa">欢 迎 登 录</div>
<el-form ref="formRef" :model="user" :rules="rules">
<el-form-item prop="username">
<el-input :prefix-icon="User" size="large" v-model="user.username" placeholder="请输入账号"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input show-password :prefix-icon="Lock" size="large" v-model="user.password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item prop="role">
<el-select size="large" v-model="user.role" placeholder="请选择角色">
<el-option value="ADMIN" label="管理员"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" style="width: 100%" @click="login">登 录</el-button>
</el-form-item>
<div style="text-align: right">
还没有账号?请 <router-link to="/register"><b style="color: dodgerblue">注册</b></router-link>
</div>
</el-form>
</div>
</div>
</template>
<style scoped>
.login-container {
height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(to top, #7f7fd5, #86a8e7, #91eae4);
}
.login-box {
width: 350px;
padding: 30px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-color: rgba(255, 255, 255, 0.5);
}
</style>
13.添加路由规则--router.js
javascript
import {createRouter,createWebHashHistory} from "vue-router";
// 在router.js文件中创建路由实例对象
const router=createRouter({
history:createWebHashHistory(),
routes:[//路由规则
{path:"/",redirect:"/login"},
{path:"/manage",component:()=>import('@/views/Manage.vue'),
children:[//子路由
{path:"index",component:()=>import('@/views/manage/Index.vue')},
{path:"goods",component:()=>import('@/views/manage/Goods.vue')},
{path:"admin",component:()=>import('@/views/manage/Admin.vue')},
{path:"personal",component:()=>import('@/views/manage/Personal.vue')},
{path:"password",component:()=>import('@/views/manage/Password.vue')},
{path:"notice",component:()=>import('@/views/manage/Notice.vue')},
]
},
{path:"/login",component:()=>import('@/views/Login.vue')},
{path:"/404",component:()=>import('@/views/404.vue')},
{path:"/register",component:()=>import('@/views/Register.vue')},
{path: '/:pathMatch(.*)', redirect: '/404' }
]
})
// 输出实例对象
export default router;
效果图:
首页:
商品信息管理:
完整项目可以从资源下载