k8s管理系统项目前端-1
k8s管理系统项目前端-1
前端使用vue3框架以及element-plus组件完成,依赖以下组件:
- xterm命令行终端模拟器
- nprogress浏览器顶部的进度条
- jwt token生成和校验组件
- json-editor-vue3/codemirror-editor-vue3代码编辑器,用于编辑k8s资源YAML
- echarts画图组件,如柱状图、饼图等
一、框架搭建
1、初始化Vue项目
1) 创建vue3项目
vue create k8s-platform-fe
2)关闭语法检查配置文件,关闭语法检测,设置端口号
vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
devServer:{
host: '0.0.0.0',//监听地址
port: 3000, // 启动端口号
open: true // 启动后是否自动打开网页
},
transpileDependencies: true,
//关闭语法检测
lintOnSave: false
})
3)初始化main.js以及安装插件
main.js
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus, {ElMessage} from 'element-plus'
import 'element-plus/dist/index.css'
import * as ELIcons from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import * as echarts from 'echarts';
const app = createApp(App)
//将图标注册为全局组件
for (let iconName in ELIcons) {
app.component(iconName, ELIcons[iconName])
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.config.globalProperties.$message = ElMessage;
app.mount('#app')
4)初始化App.vue
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<router-view></router-view>
</template>
<style>
/*设置html和body*/
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
</style>
2、编写主页左侧菜单
src/views/HomeView.vue
<template>
<div class="common-layout">
<!-- container整体布局 -->
<el-container style="height: 100vh;">
<!-- 侧边栏,定义默认宽度 -->
<el-aside class="aside" :width="asideWidth">
<!-- 固钉,将平台logo和名字固钉在侧边栏最上方 -->
<!-- z-index是显示优先级 -->
<el-affix class="aside-affix" :z-index="1200">
<div class="aside-logo">
<!-- logo图片 -->
<img class="logo-image" :src="logo" />
<!-- 平台名,折叠后不显示 -->
<span :class="[isCollapse ? 'is-collapse' : '']">
<span class="logo-name">Kubernetes</span>
</span>
</div>
</el-affix>
<!-- 菜单导航栏 -->
<!-- router 使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转 -->
<!-- default-active 当前激活菜单的index,将菜单栏与路径做了对应关系 -->
<!-- collapse 是否折叠 -->
<el-menu class="aside-menu"
router
:default-active="$route.path"
:collapse="isCollapse"
background-color="#131b27"
text-color="#bfcbd9"
active-text-color="#20a0ff">
<!-- for循环路由规则 -->
<div v-for="menu in filteredRouters" :key="menu.path">
<!-- 处理子路由只有1个的情况,如概要、工作流 -->
<el-menu-item class="aside-menu-item" v-if="menu.children && menu.children.length == 1" :index="menu.children[0].path">
<!-- 引入图标的方式 -->
<el-icon><component :is="menu.children[0].icon" /></el-icon>
<template #title>
{{menu.children[0].name}}
</template>
</el-menu-item>
<!-- 处理有多个子路由的情况,如集群、工作负载、负载均衡等 -->
<!-- 父菜单 -->
<!-- 注意el-menu-item在折叠后,title的部分会自动消失,但el-sub-menu不会,需要自己控制 -->
<el-sub-menu class="aside-submenu" v-else-if="menu.children && menu.children.length > 1" :index="menu.path">
<template #title>
<el-icon><component :is="menu.icon" /></el-icon>
<span :class="[isCollapse ? 'is-collapse' : '']">{{menu.name}}</span>
</template>
<!-- 子菜单 -->
<el-menu-item class="aside-menu-childitem" v-for="child in menu.children" :key="child" :index="child.path">
<template #title>
{{child.name}}
</template>
</el-menu-item>
</el-sub-menu>
</div>
</el-menu>
</el-aside>
<!-- header、main、以及footer -->
<el-container>
<!-- header -->
<el-header class="header">
<el-row :gutter="20">
<el-col :span="1">
<!-- 折叠按钮 -->
<div class="header-collapse" @click="onCollapse">
<el-icon><component :is="isCollapse ? 'expand':'fold'" /></el-icon>
</div>
</el-col>
<el-col :span="10">
<!-- 面包屑 -->
<div class="header-breadcrumb">
<!-- separator 分隔符 -->
<el-breadcrumb separator="/">
<!-- :to="{ path: '/' }"表示跳转到/路径 -->
<el-breadcrumb-item :to="{ path: '/' }">工作台</el-breadcrumb-item>
<template v-for="(matched,m) in this.$route.matched" :key="m">
<el-breadcrumb-item v-if="matched.name != undefined">
{{ matched.name }}
</el-breadcrumb-item>
</template>
</el-breadcrumb>
</div>
</el-col>
<el-col class="header-menu" :span="13">
<!-- 用户信息 -->
<el-dropdown>
<!-- 头像及用户名 -->
<div class="header-dropdown">
<el-image class="avator-image" :src="avator" />
<span>{{ username }}</span>
</div>
<!-- 下拉框内容 -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="logout()">退出</el-dropdown-item>
<el-dropdown-item >修改密码</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
</el-row>
</el-header>
<!-- main -->
<el-main class="main">
<!-- 路由占位符,展示匹配到的路由的视图组件 -->
<router-view></router-view>
</el-main>
<!-- footer -->
<el-footer class="footer">
<!-- <el-icon style="width:2em;top:3px;font-size:18px"><place/></el-icon>-->
<a class="footer el-icon-place"></a>
</el-footer>
<!-- 返回顶部,其实是返回el-main的顶部 -->
<el-backtop target=".el-main"></el-backtop>
</el-container>
</el-container>
</div>
</template>
<script setup>
import { computed, ref, onBeforeMount } from 'vue';
import { useRoute,useRouter } from 'vue-router';
// import router from '@/router';
import avator from '@/assets/avator.png'
import logo from '@/assets/k8s-metrics.png'
// 使用ref创建响应式引用
const isCollapse = ref(false);
const asideWidth = ref('220px');
// const routers = ref([]);
const router = useRouter(); // 获取router实例
const route = useRoute(); // 获取当前路由
const routers = ref(router.options.routes) // 假设这是您的路由数组
// 计算属性,过滤出未隐藏的路由
function filterRoutes(routes) {
return routes
.filter((r) => !r.meta?.hidden)
.map((r) => {
if (r.children && r.children.length) {
return {
...r,
children: filterRoutes(r.children)
};
}
return r;
});
}
const filteredRouters = computed(() => filterRoutes(routers.value));
// 计算属性获取用户名
const username = computed(() => {
let username = localStorage.getItem('username');
return username ? username : '未知';
});
// 监听生命周期钩子
// onBeforeMount(() => {
// const route = useRouter();
// routers.value = route.options.routes.filter(r => !r.hidden);
// });
// 控制导航栏折叠
function onCollapse() {
if (isCollapse.value) {
asideWidth.value = '220px';
isCollapse.value = false;
} else {
isCollapse.value = true;
asideWidth.value = '64px';
}
}
// 登出方法
function logout() {
localStorage.removeItem('username');
localStorage.removeItem('token');
router.push('/login');
}
</script>
<style scoped>
/* 侧边栏折叠速度,背景色 */
.aside{
transition: all .5s;
background-color: #131b27;
}
/* 固钉,以及logo图片和平台名的属性 */
.aside-logo{
background-color: #131b27;
height: 60px;
color: white;
}
.logo-image {
width: 40px;
height: 40px;
top: 12px;
margin-top: 10px;
padding-left: 12px;
}
.logo-name{
font-size: 20px;
font-weight: bold;
padding: 10px;
position: relative;
top: -12px;
}
/* 滚动条不展示 */
.aside::-webkit-scrollbar {
display: none;
}
/* 修整边框,让边框不要有溢出 */
.aside-affix {
border-bottom-width: 0;
}
.aside-menu {
border-right-width: 0
}
/* 菜单栏的位置以及颜色 */
.aside-menu-item.is-active {
background-color: #1f2a3a ;
}
.aside-menu-item {
padding-left: 20px !important;
}
.aside-menu-item:hover {
background-color: #142c4e ;
}
.aside-menu-childitem {
padding-left: 40px !important;
}
.aside-menu-childitem.is-active {
background-color: #1f2a3a ;
}
.aside-menu-childitem:hover {
background-color: #142c4e ;
}
/* header的属性 */
.header{
z-index: 1200;
line-height: 60px;
font-size: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)
}
/* 折叠按钮 */
.header-collapse{
cursor: pointer;
}
/* 面包靴 */
.header-breadcrumb{
padding-top: 0.9em;
}
/* 用户信息靠右 */
.header-menu{
text-align: right;
}
/* 折叠属性 */
.is-collapse {
display: none;
}
/* 用户信息下拉框 */
.header-dropdown {
line-height: 60px;
cursor: pointer;
}
/* 头像 */
.avator-image {
top: 12px;
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 8px;
}
.header-dropdown{
outline: none;
}
.main {
padding: 10px;
}
.footer {
z-index: 1200;
color: rgb(187, 184, 184);
font-size: 14px;
text-align: center;
line-height: 60px;
}
</style>
router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import LoginView from "@/views/LoginView.vue";
import HomeView from "@/views/HomeView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: LoginView,
meta: {},
},
{
path: '/',
redirect: '/Summary',
meta: {},
},
{
path: "/Summary",
icon: "odometer",
component:HomeView,
meta: {},
children:[
{
icon: "odometer",
path: "/Summary",
name: '概要',
meta: {title: "概要", requireAuth: true},
component: () => import('../views/Summary/SummaryView.vue'),
}
]
},
{
path: '/workflow',
component:HomeView,
icon: "VideoPlay",
meta: {},
children: [
{
path: "/workflow",
name: "工作流",
icon: "VideoPlay",
meta: {title: "工作流", requireAuth: true},
component: () => import('../views/workflow/workflowView.vue'),
}
]
},
{
path: "/cluster",
name: "集群",
component:HomeView,
icon: "home-filled",
meta: {title: "集群", requireAuth: true},
children: [
{
path: "/cluster/node",
name: "Node",
icon: "el-icon-s-data",
meta: {title: "Node", requireAuth: true},
component: () => import("@/views/cluster/nodeView.vue")
},
{
path: "/cluster/namespace",
name: "Namespace",
icon: "el-icon-document-add",
meta: {title: "创建namespace", requireAuth: true},
component: () => import("@/views/cluster/namespaceView.vue"),
},
]
},
{
path: "/workload",
name: "工作负载",
component:HomeView,
icon: "menu",
meta: {title: "工作负载", requireAuth: true},
children: [
{
path: "/workload/deployment",
name: "Deployment",
icon: "el-icon-s-data",
meta: {title: "Deployment", requireAuth: true},
component: () => import("@/views/workload/deploymentView.vue"),
},
{
path: "pod/:type/:deploymentName/:namespace",
name: "Pod",
icon: "el-icon-document-add",
meta: {title: "Pod", requireAuth: true,hidden: true},
props: true,
component: () => import("@/views/workload/podView.vue")
},
{
path: "/workload/deamonset",
name: "DaemonSet",
icon: "el-icon-document-add",
meta: {title: "DaemonSet", requireAuth: true},
component: () => import("@/views/workload/daemonsetView.vue")
},
{
path: "/workload/statefulset",
name: "StatefulSet",
icon: "el-icon-document-add",
meta: {title: "DaemonSets", requireAuth: true},
component: () => import("@/views/workload/statefulsetView.vue")
}
]
},
{
path: "/loadbalance",
name: "负载均衡",
component:HomeView,
icon: "files",
meta: {title: "负载均衡", requireAuth: true},
children: [
{
path: "/loadbalance/service",
name: "Service",
icon: "el-icon-s-data",
meta: {title: "Service", requireAuth: true},
component: () => import("@/views/loadbalance/serviceView.vue")
},
{
path: "/loadbalance/ingress",
name: "Ingress",
icon: "el-icon-document-add",
meta: {title: "Ingress", requireAuth: true},
component: () => import("@/views/loadbalance/ingressView.vue")
}
]
},
{
path: "/storage",
name: "存储与配置",
component:HomeView,
icon: "tickets",
meta: {title: "存储与配置", requireAuth: true},
children: [
{
path: "/storage/configmap",
name: "Configmap",
icon: "el-icon-document-add",
meta: {title: "Configmap", requireAuth: true},
component: () => import("@/views/storage/configmapView.vue")
},
{
path: "/storage/secret",
name: "Secret",
icon: "el-icon-document-add",
meta: {title: "Secret", requireAuth: true},
component: () => import("@/views/storage/secretView.vue")
},
{
path: "/storage/persistentvolume",
name: "PersistentVolume",
icon: "el-icon-document-add",
meta: {title: "PersistemtVolume", requireAuth: true},
component: () => import("@/views/storage/persistentvolumeView.vue")
},
{
path: "/storage/persistentvolumeclaim",
name: "PersistentVolumeClaim",
icon: "el-icon-s-data",
meta: {title: "PersistentVolumeClaim", requireAuth: true},
component: () => import("@/views/storage/persistentvolumeClainView.vue")
},
]
},
{
path: "/users",
name: "用户管理",
component:HomeView,
icon: "avatar",
meta: {title: "存储与配置", requireAuth: true},
children: [
{
path: "/users/user",
name: "用户",
icon: "el-icon-document-add",
meta: {title: "用户", requireAuth: true},
component: () => import("@/views/user/userView.vue")
},
{
path: "user/:id",
name: "userDetail",
icon: "el-icon-document-add",
meta: {title: "用户详情", requireAuth: true,hidden: true},
props: true,
component: () => import("@/views/user/userInfoView.vue")
},
{
path: "/user/group",
name: "用户组",
icon: "el-icon-document-add",
meta: {title: "用户组", requireAuth: true},
component: () => import("@/views/user/groupView.vue")
},
{
path: "/user/settings",
name: "用户设置",
icon: "el-icon-s-data",
meta: {title: "用户设置", requireAuth: true},
component: () => import("@/views/user/userSetView.vue")
},
]
},
{
path: '/404',
component: () => import('@/views/common/404.vue'),
meta: {
title: '404'
}
},
{
path: '/403',
component: () => import('@/views/common/403.vue'),
meta: {
title: '403'
}
},
//其他路径跳转至404页面
{
path: '/:pathMatch(.*)',
redirect: '/404'
},
]
})
export default router
3、封装axios
封装axios请求,添加自定义配置、拦截器、token过期等
api/client.js
import axios from "axios";
import { ElMessage } from 'element-plus';
import router from "@/router";
// instance.get('/api/vlog/v1/blogs')
var instance = axios.create({
// 后端的URL 地址,沿用vite配置
// 比如前段是http://localhost:5173/login
// http://localhost:5173/api/v1/vblog
// vite 会进行拦截,将/api/v1 前面的替换成vite配置 http://127.0.0.1:9999/api/v1/vblog
baseURL: '',
// 超时时间5秒
timeout:5000,
// 后端Gin 使用的Bind函数,而不是BindJson,补充请求Data是哪个格式
headers:{'Content-Type': 'application/json'}
});
// 请求拦截器
// instance.interceptors.request
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 假设token存储在localStorage中,也可以存储在Vuex store或其他地方
const token = localStorage.getItem('token');
if (token) {
// 将token设置在headers中
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
// 处理请求错误
return Promise.reject(error);
}
);
// 通过相应拦截器统一处理异常
instance.interceptors.response.use(
(resp) =>{
console.log(resp)
return resp.data
},
(error)=>{
let msg
console.log("error = ",error)
if (error.response.data && error.response.data.message) {
// 通用错误处理
msg = error.response.data.message
}
console.log("msg1 = ",msg)
// 处理自定义异常
switch (error.response.data.code){
// token 过期
case 500:
localStorage.removeItem('token')
router.push({name:'login'})
break
}
// 直接把异常信息提示出来
ElMessage.error(`${msg}`)
// 是否要注入error,业务页面需要拿到异常
return Promise.reject(error.response.data)
},
)
export default instance
3、403页面
views/common/403.vue
<template>
<div class="main-body-div">
<el-row>
<!-- 图片 -->
<el-col :span="24">
<div>
<img class="main-body-img" src="../../assets/img/403.png" />
</div>
</el-col>
<el-col :span="24">
<!-- 描述 -->
<div>
<p class="status-code">403</p>
<p class="status-describe">你暂时无权限访问该页面······</p>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
/* 图片属性 */
.main-body-img {
margin-top: 150px
}
/* 整体位置 */
.main-body-div {
text-align: center;
height: 100vh;
width: 100vw;
}
/* 状态码 */
.status-code {
margin-top: 20px;
margin-bottom: 10px;
font-size: 95px;
font-weight: bold;
color: rgb(54, 95, 230);
}
/* 描述 */
.status-describe {
color: rgb(145, 143, 143);
}
</style>
4、404页面
views/common/404.vue
<template>
<div class="main-body-div">
<el-row>
<!-- 图片 -->
<el-col :span="24">
<div>
<img class="main-body-img" src="../../assets/img/404.png" />
</div>
</el-col>
<!-- 描述 -->
<el-col :span="24">
<div>
<p class="status-code">404</p>
<p class="status-describe">你所访问的页面不存在······</p>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
/* 图片属性 */
.main-body-img {
margin-top: 15%;
}
/* 整体位置 */
.main-body-div {
text-align: center;
height: 100vh;
width: 100vw;
}
/* 状态码 */
.status-code {
margin: 20px 0 20px 0;
font-size: 95px;
font-weight: bold;
color: rgb(54, 95, 230);
}
/* 描述 */
.status-describe {
color: rgb(145, 143, 143);
}
</style>
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 J.のblog!
评论