k8s管理系统项目前端-1

前端使用vue3框架以及element-plus组件完成,依赖以下组件:

  • xterm命令行终端模拟器
  • nprogress浏览器顶部的进度条
  • jwt token生成和校验组件
  • json-editor-vue3/codemirror-editor-vue3代码编辑器,用于编辑k8s资源YAML
  • echarts画图组件,如柱状图、饼图等

image

image

一、框架搭建

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>