概述
一直以来,我都想做一套属于自己的后台管理框架,那么,今天,2023/5/27日,jkw框架就应运而生了。此框架,前端我用vue做,后端测试接口用的java技术。来看看今天都干了什么吧!
项目搭建
创建项目【项目名为vue-jkw】
npm init vue@latest
创建项目时依赖选用
这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示
✔ Project name: … (项目名,不要存在大写)
✔ Add TypeScript? … No / Yes(no,直接回车)
✔ Add JSX Support? … No / Yes(no,直接回车)
✔ Add Vue Router for Single Page Application development? … No / Yes 集成路由
✔ Add Pinia for state management? … No / Yes(no,直接回车)Pinia
✔ Add Vitest for Unit testing? … No / Yes(no,直接回车)
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes(no,直接回车)
✔ Add ESLint for code quality? … No / Yes(no,直接回车)
✔ Add Prettier for code formatting? … No / Yes(no,直接回车)
安装依赖
cd vue-jkw
npm install
启动开发服务器(项目目录)
npm run dev
项目效果【2023/5/27】
登录页面
首页
用户管理界面
对话框
代码公开
登录页面的代码
<template>
<div class="login-container">
<el-form class="user" size="large" :model="user">
<div class="title-container">
<h3 class="title">LOGIN</h3>
</div>
<div class="input">
<el-form-item prop="username">
<el-input :prefix-icon="User" type="text" v-model="user.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input :prefix-icon="Lock" type="password" v-model="user.password" placeholder="请输入密码"></el-input>
</el-form-item>
</div>
<!--由于el-button是放在表单里,默认是会实现表单的跳转,需要用@click.native.prevent阻止-->
<el-button style="width:100%;margin-bottom:30px;" round type="primary"
@click.native.prevent="handleLogin">登录</el-button>
</el-form>
</div>
</template>
<script setup>
import { reactive } from 'vue';
import { useRouter } from "vue-router"//router
import axios from "@/utils/request.js"//引入封装的axios
import { useLoginStore } from "@/stores/loginStore.js"//stores
import { User, Lock } from '@element-plus/icons-vue'//输入框引入字体图标,使用:prefix-icon="User"
const loginStore = useLoginStore()
//获取路由对象
const router = useRouter()
//声明用户信息
const user = reactive({
username: "",
password: ""
})
//登录点击事件
const handleLogin = () => {
axios.post('admin/login', {
username: user.username,
password: user.password
}).then(res => {
if (res.data.code === 200) {
loginStore.username = res.data.data.username;
loginStore.islogin = true
router.push("/")
} else {
ElMessage.error(res.data.message)
}
})
}
</script>
<style scoped>
.login-container {
/*在App.vue中设置公共样式:顶级容器body,html,#app的高度为100%,使其背景布满全屏 */
width: 100%;
height: 730px;
/* border-radius: 15px; */
background-image: linear-gradient(135deg, #EE9AE5 10%, #5961F9 100%) !important;
}
.user {
/**相对定位 */
position: relative;
width: 400px;
/*设置内边距:上 左右 下 */
padding: 160px 35px 0;
/*居中:外边距上下-0 左右自动居中*/
margin: 0 auto;
}
.title-container .title {
font-family:楷体;
font-size: 40px;
font-weight: 900;
color: #fff;
text-align: center;
margin: 20px;
}
</style>
侧边栏代码
<template>
<!--添加动画效果:设置内部样式-->
<div class="slider-navs" :style="{ width: menuStore.isCollapse ? '64px' : '210px' }">
<div v-if="menuStore.toggleStore" class="logo">{{ menuStore.isCollapse ? 'JKW' : "J K W" }}</div>
<!--background-color:背景颜色(只有菜单这块,菜单占多少背景占多少)
text-color:文本颜色
active-text-color:选中后文本颜色
:default-active="active":点击高亮(与index=""搭配,在script设置默认访问/)
router:可以把点击高亮里的index的地址变为路由地址,进行路由访问
:collapse:是否开启折叠面板
unique-opened 是否只保持一个子菜单的展开
点击的是el-sub-menu,所以el-sub-menu 的唯一性是必须的,设置index属性来保持唯一性
-->
<el-menu background-color="rgba(255,255,255,.1)" text-color="#ffff" active-text-color="orange"
:default-active="active" router class="el-menu-vertical-demo" :collapse="menuStore.isCollapse" unique-opened>
<el-menu-item index="/home/index">
<el-icon>
<HomeFilled />
</el-icon>
<span>首页</span>
</el-menu-item>
<el-sub-menu index="1">
<template #title>
<el-icon>
<Tools />
</el-icon>
<span>系统</span>
</template>
<el-menu-item index="/sys/admin">
<el-icon>
<User />
</el-icon>
<span>用户</span>
</el-menu-item>
<el-menu-item index="/sys/role">
<el-icon>
<CopyDocument />
</el-icon>
<span>角色</span>
</el-menu-item>
<el-menu-item index="/sys/permission">
<el-icon>
<Lock />
</el-icon>
<span>权限</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useMenuStore } from "@/stores/menuStore.js";//stores
//解决页面刷新后 菜单高亮与面包屑不匹配
const menuStore = useMenuStore()
const active = ref("/")
if (localStorage.getItem("active")) {
active.value = localStorage.getItem("active")
}
</script>
<style scoped>
.slider-navs {
background-image: linear-gradient(135deg, #EE9AE5 10%, #5961F9 100%) !important;
/*固定在左侧(position: fixed生成绝对定位的元素, 相对于浏览器窗口进行定位) */
position: fixed;
left: 0;
top: 0;
bottom: 0;
/*width: 210px;*/
/*设置动画过渡 和<el-menu>内部的一样 */
transition: 0.3s ease-in;
}
.el-menu-vertical-demo {
background-image: linear-gradient(to bottom right, rgb(114, 135, 254), rgb(130, 88, 186));
}
.logo {
background-image: linear-gradient(120deg, #00e4d0, #5983e8) !important;
/*需要在App.vue设置公共样式,把element menu的右边框去掉,否则对不齐 */
width: 100%;
height: 60px;
background-color: #364e6d;
font-size: 25px;
color: #fff;
text-align: center;
/*左右居中 */
line-height: 60px;
/*上下居中:和高度保持一致 */
}
.icon {
width: 16px;
height: 16px;
margin-right: 5px;
/*让图标和文字有空隙 */
}
</style>
头部导航栏代码
<template>
<div class="nav">
<div class="toggle-menu">
<div class="toggle-menu-toggle">
<!--折叠菜单 图标-->
<el-icon v-if="menuStore.isCollapse" class="icon" @click="openMenu(false)">
<Expand />
</el-icon>
<el-icon v-else class="icon" @click="closeMenu(true)">
<Fold />
</el-icon>
</div>
</div>
<div class="toggle-menu-breadcrumb">
<el-breadcrumb separator="/">
<el-breadcrumb-item>{{ $t("message.navs") }}</el-breadcrumb-item>
<el-breadcrumb-item>{{ menuStore.breadcrumb }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="lang">
<el-dropdown>
<span class="el-dropdown-link">
语言
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-item @click="changeLang('zh')">
简体中文
</el-dropdown-item>
<el-dropdown-item @click="changeLang('en')">
English
</el-dropdown-item>
</template>
</el-dropdown>
</div>
<div class="login" v-if="!loginStore.islogin">
<el-button type="success" round plain @click="loginHandler">立即登录</el-button>
</div>
<div class="user" v-if="loginStore.islogin">
<el-dropdown>
<span class="el-dropdown-link">
<el-icon>
<Avatar />
</el-icon>
<!--动态显示用户名-->
{{ loginStore.username }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-icon>
<UserFilled />
</el-icon>
<router-link to="/home/center">个人中心</router-link>
</el-dropdown-item>
<el-dropdown-item @click="logoutHandler">
<el-icon>
<SwitchButton />
</el-icon>退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import { useLoginStore } from "@/stores/loginStore.js"//stores
import { useMenuStore } from "@/stores/menuStore.js"
import { useRouter } from "vue-router"//router
import axios from "@/utils/request.js"
const loginStore = useLoginStore()
const router = useRouter()
const menuStore = useMenuStore()
//登录
const loginHandler = () => {
router.push("/home/login")
}
/*退出登录*/
const logoutHandler = () => {
axios.get('/admin/logout').then(res => {
console.log(res.data)
if (res.data.code == 200) {
//把仓库中登录信息清空,并返回登录页
loginStore.islogin = false
loginStore.username = ""
router.push("/")
} else {
ElMessage.error(res.data.message)
}
}).catch(error => {
console.log(error);
})
}
/*关闭左侧菜单栏 */
const closeMenu = (flag) => {
//点击后把isCollapse值赋予true,关闭左侧菜单
menuStore.isCollapse = flag
}
/*打开菜单栏 */
const openMenu = (flag) => {
//点击后把isCollapse值赋予false,关闭左侧菜单
menuStore.isCollapse = flag
}
//切换语言
const changeLang = (lang) => {
//console.log(lang);
localStorage.setItem("lang", lang)
//语言切换刷新ui
location.reload();
}
</script>
<style scoped>
.nav {
background-image: linear-gradient(120deg, #00e4d0, #5983e8) !important;
width: 100%;
background-color: #fff;
/*height:和左侧菜单的logo高度一致 */
height: 60px;
/*最下面阴影效果 */
box-shadow: 0 1px 3px 0 rgb(0 0 0/ 12%), 0 0 3px 0 rgb(0 0 0 /4%);
}
.toggle-menu {
/*图标居中 */
padding-top: 17.5px;
padding-left: 10px;
}
.icon {
/*改变图标大小 */
font-size: 25px;
}
.toggle-menu-toggle {
/*向左浮动,水平结构 */
float: left;
}
.toggle-menu-breadcrumb {
/*向左浮动,水平结构 */
float: left;
/*面包屑居中 */
line-height: 60px;
margin-top: 6px;
margin-left: 20px;
}
.login {
position: absolute;
right: 40px;
top: 15px;
}
.user {
/*下拉框大小 */
font-size: 15px;
/*下拉框放到左边 */
position: absolute;
right: 40px;
/*下拉框居中*/
top: 23px;
}
.lang {
float: right;
margin-right: 180px;
margin-top: 5px;
}
</style>
echarts数据公开
/**
* echarts图标库
*/
import * as echarts from "echarts"//基本图标
export default {
/**echarts挂载到vue全局 */
install: app => {
//折线图
app.config.globalProperties.$line = (element, data) => {
var myChart = echarts.init(document.getElementById(element))
const colors = ['#5470C6', '#EE6666'];
const option = {
color: colors,
tooltip: {
trigger: 'none',
axisPointer: {
type: 'cross'
}
},
legend: {},
grid: {
top: 70,
bottom: 50
},
xAxis: [
{
type: 'category',
axisTick: {
alignWithLabel: true
},
axisLine: {
onZero: false,
lineStyle: {
color: colors[1]
}
},
axisPointer: {
label: {
formatter: function (params) {
return (
'Precipitation ' +
params.value +
(params.seriesData.length ? ':' + params.seriesData[0].data : '')
);
}
}
},
// prettier-ignore
data: ['2016-1', '2016-2', '2016-3', '2016-4', '2016-5', '2016-6', '2016-7', '2016-8', '2016-9', '2016-10', '2016-11', '2016-12']
},
{
type: 'category',
axisTick: {
alignWithLabel: true
},
axisLine: {
onZero: false,
lineStyle: {
color: colors[0]
}
},
axisPointer: {
label: {
formatter: function (params) {
return (
'Precipitation ' +
params.value +
(params.seriesData.length ? ':' + params.seriesData[0].data : '')
);
}
}
},
// prettier-ignore
data: ['2015-1', '2015-2', '2015-3', '2015-4', '2015-5', '2015-6', '2015-7', '2015-8', '2015-9', '2015-10', '2015-11', '2015-12']
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'Precipitation(2015)',
type: 'line',
xAxisIndex: 1,
smooth: true,
emphasis: {
focus: 'series'
},
data: [
2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3
]
},
{
name: 'Precipitation(2016)',
type: 'line',
smooth: true,
emphasis: {
focus: 'series'
},
data: [
3.9, 5.9, 11.1, 18.7, 48.3, 69.2, 231.6, 46.6, 55.4, 18.4, 10.3, 0.7
]
}
]
};
myChart.setOption(option)
},
//雷达图
app.config.globalProperties.$radar = (element, data) => {
var myChart = echarts.init(document.getElementById(element))
const option = {
legend: {
data: ['Allocated Budget', 'Actual Spending']
},
radar: {
// shape: 'circle',
indicator: [
{ name: 'Sales', max: 6500 },
{ name: 'Administration', max: 16000 },
{ name: 'Information Technology', max: 30000 },
{ name: 'Customer Support', max: 38000 },
{ name: 'Development', max: 52000 },
{ name: 'Marketing', max: 25000 }
]
},
series: [
{
name: 'Budget vs spending',
type: 'radar',
data: [
{
value: [4200, 3000, 20000, 35000, 50000, 18000],
name: 'Allocated Budget'
},
{
value: [5000, 14000, 28000, 26000, 42000, 21000],
name: 'Actual Spending'
}
]
}
]
}
myChart.setOption(option)
},
//饼状图
app.config.globalProperties.$rose = (element, data) => {
var myChart = echarts.init(document.getElementById(element))
const option = {
tooltip: {
trigger: 'item'
},
legend: {
top: '5%',
left: 'center'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
]
}
]
}
myChart.setOption(option)
},
//柱状图
app.config.globalProperties.$bar = (element, data) => {
var myChart = echarts.init(document.getElementById(element))
const option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar'
}
]
}
myChart.setOption(option)
}
}
}
pinia代码
import { defineStore } from "pinia"
export const useLoginStore = defineStore("login", {
state: () => {
return {
username: "",//用户名
islogin:false//是否登录
}
},
//本地持久化(把数据存储到浏览器本地)
persist: {
enabled: true,//是否开启持久化
strategies: [
{
key: 'login', //自定义Key值,存储到本地时的key
storage: localStorage, // 选择存储方式:本地存储
},
],
}
})