K8S管理系统项目实战[前端开发]

前端

Vue 入门与进阶
Vue ElementPlus 组件库 
K8s管理系统项目实战[前端开发]

前端代码仓库地址 GitHub - yunixiangfeng/k8s-platform-fe

项目概述、框架搭建

Vue前端开发:整体布局

Vue前端开发:工作负载

Vue前端开发:仪表盘

Vue前端开发:工作流

Vue前端开发:登录登出、部署、总结

项目概述、框架搭建

一、项目慨述

本节是k8s管理系统项目实战的前端开发部分,在完成API接口的整体开发后,开始看手于前端部分,构建一个个功能页面,将管理系统平台化。
前端部分使用vue3框架以及element-plus组件完成,开发过程中,会使用到以下依赖:
(1) xterm 命令行终端模拟器
(2) nprogress 浏览器顶部的进度条
(3) jsonwebtoken jwt token的生成与校验组件
(4) json-editor-vue3/codemirror-editor-vue3 代码编辑器,用于编辑k8s资源YAML
(5) echarts 画图组件,如柱状图、饼图等

二、Vue目录结构及启动

1、目录结构

 

node_modules:存放npm下载的依赖包
public: 站点图标和主页
package.json/package-lock.json:存放依赖版本及项目描述信息

babel.config.js: babel的配置文件,babel是js的编译器

vue.config.js:vue的配置文件

src/下:
views/common/Config,js: 存放后端接口路径、编辑器配置等公共属性
assets:存放图片等静态资源
components: 存放自定义的公共组件

layout: 存放布局视图文件
router: 定义路由配置及规则
utils: 工具类,用于常用方法的封装

views:存放各个页面的视图文件
App.vue:主组件,所有页面都是在App.vue下进行切换,可以理解为所有的路由都是App.vue的子组件

main.js:入口文件,主要作用是初始化vue实例,并引入所需插件

2、启动过程

三、开发&响应流程

 

index.html:public目录下的项目html的入口文件

App.vue: 主组件,所有views下的页面都是在App.vue下进行切换

router/index.js:定义路由配置及规则

src/views/xxvue:各个页面的视图文件

四、框架搭建

 1、初始化Vue项目

 (1)初始化vue3项目

安装vue

npm install @vue/cli -g
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: 7707,           // 监听端口
    open: true            // 启动后是否打开网页
  },
  transpileDependencies: true,
  // 关闭语法检测
  lintOnSave: false
})

(3)初始化main.js以及安装插件

main.js

import { createApp } from 'vue'
import App from './App.vue'
// 代码引入element plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//引入图标视图
import * as ELIcons from '@element-plus/icons-vue'
//引入路由配置和规则
import router from "./router"

// 创建app实例
const app = createApp(App)
// 图标注册为全局组件
for (let iconName in ELIcons) {
    app.component(iconName, ELIcons[iconName])
}
app.use(ElementPlus)
app.use(router)


// 挂载
app.mount('#app')

 安装插件

npm install element-plus \
npm install vue-router \
npm install nprogress \
npm install axios \
npm install json2yaml \
npm install js-yaml

(4)初始化App.vue

<template>
  <span>我是App.....</span>
  <!--  路由占位符-->
  <router-view></router-view>
</template>

<style>
  .html, body{
    width: 100%;
    height: 100%;
    padding: 0;
    margin: 0;
  }
  #nprogress .bar {
    /* 自定义进度条 */
    background: #2186c0 !important;
  }
</style>

创建目录views、router、layout、utils

创建router/index.js

启动vue项目

npm run serve

2、封装路由

src/view/home/Home.vue

<template>
<div class="home">
  我是Home.vue
</div>
</template>

 src/router/index.js

import {createRouter, createWebHistory} from 'vue-router'

//定义路由规则
const routes = [
    {
        path: '/home', //视图
        component: () => import('@/views/home/Home.vue'),
        icon: "odometer", //图标
        meta: {title:"概要", requireAuth: false}, //定义meta元数据
    },
]

//创建路由实例
const router = createRouter({
    history: createWebHistory(),
    routes
})


export default router

3、添加进度条

 src/router/index.js

// 导入进度条
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'


//进度条配置
NProgress.inc(0.2) //设置进度条递增
NProgress.configure({easing: 'ease', speed: 600, showSpinner: false})//动画效果、动画速度、进度环是否显示

//路由守卫,路由拦截
router.beforeEach((to, from, next) => {
    //启动进度条
    NProgress.start()
    //设置头部
    if (to.meta.title) {
        document.title = to.meta.title
    } else {
        document.title = "Kubernetes"
    }
    // 放行
    next()
})

//关闭进度条
router.afterEach(() => {
    NProgress.done()
})

4、启动/测试

npm run serve

5、封装axios

封装axios请求,添加自定义配置,如超时、重试、header等

utils/request.js

import axios from 'axios'

//新建个axios对象
const httpClient = axios.create({
    validateStatus(status) {
        return status >= 200 && status < 504 //设置默认的合法状态码,不合法的话不接受response
    },
    timeout: 10000 //超时时间10秒
})

httpClient.defaults.retry = 3 // 请求重试次数
httpClient.defaults.retryDelay = 1000 //请求重试时间间隔
httpClient.defaults.shouldRetry = true //是否重试

//添加请求拦截器
httpClient.interceptors.request.use(
    config => {
        //添加header
        config.headers['Content-Type'] = 'application/json'
        config.headers['Accept-Language'] = 'zh-CN'
        config.headers['Authorization'] = localStorage.getItem("token")
        //处理post请求
        if(config.method == 'post') {
            if (!config.data) {
                config.data = {}
            }
        }
        return config
    },
    err => {
        return Promise.reject(err)
    }
)

//添加响应拦截器
httpClient.interceptors.response.use(
    response => {
        //处理状态码
        if (response.status !== 200) {
            return Promise.reject(response.data)
        } else {
            return response.data
        }
    },
    err => {
        return Promise.reject(err)
    }
)

export default httpClient

6、处理404页面

 (1)404页面

src/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: 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>

 添加路由规则

src/router/index.js

//定义路由规则
const routes = [
    {
        path: '/home', //视图
        component: () => import('@/views/home/Home.vue'),
        icon: "odometer", //图标
        meta: {title:"概要", requireAuth: false}, //定义meta元数据
    },
    {
        path: '/404',
        component: () => import('@/views/common/404.vue'),
        meta: {title:"404",requiredAuth:true},
    },
    {
        path: '/:pathMatch(.*)',
        redirect: '/404',
    }

]

(2)403页面

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: 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>

 (3)路由规则

src/router/index.js

//定义路由规则
const routes = [
    {
        path: '/home', //视图
        component: () => import('@/views/home/Home.vue'),
        icon: "odometer", //图标
        meta: {title:"概要", requireAuth: false}, //定义meta元数据
    },
    {
        path: '/404',
        component: () => import('@/views/common/404.vue'),
        meta: {title:"404",requiredAuth:true},
    },
    {
        path: '/403',
        component: () => import('@/views/common/403.vue'),
        meta: {title:"403",requiredAuth:true},
    },
    {
        path: '/:pathMatch(.*)',
        redirect: '/404',
    }

]

Vue前端开发:整体布局

五、前端开发

1、整体布局

 

 (1)Container布局框架

src/layout/Layout.vue

<template>
  <div class="common-layout">
    <el-container>
      <el-side width="200">Aside</el-side>
      <el-container>
        <el-header>Header</el-header>
        <el-main>Main</el-main>
        <el-footer>Footer</el-footer>
      </el-container>
    </el-container>
  </div>
</template>

(2)添加路由规则

src/router/index.js

    {
        path: '/layout', 
        component: () => import('@/layout/Layout.vue'),
        icon: "odometer", //图标
        meta: {title:"Layout", requireAuth: false}, //定义meta元数据
    },

 将home放到main中

<template>
  <div class="common-layout">
    <el-container>
      <el-side width="200">Aside</el-side>
      <el-container>
        <el-header>Header</el-header>
        <el-main>
           <router-view></router-view> 
        </el-main>
        <el-footer>Footer</el-footer>
      </el-container>
    </el-container>
  </div>
</template>

 (3)菜单导航栏
功能:固钉、vuerouter模式的menu、折叠

Aside实现原理

上传2个图片

  1. 固钉的k8s图片logo,src/assets/img/k8s-metris.png
  2. 登录用户的头像,src/assets/img/avator.jpg

 实现固定和menu循环

src/layout/Layout.vue


<template>
  <div class="common-layout">
    <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图片 -->
            <el-image 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 routers" :key="menu">
            <!-- 处理子路由只有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>
      <el-container>
        <el-header>Header</el-header>
        <el-main>
          <router-view></router-view>
        </el-main>
        <el-footer>Footer</el-footer>
      </el-container>
    </el-container>
  </div>
</template>


<script>
import {useRouter} from 'vue-router'
export default {
  data() {
    return {
      //导入头像图片
      avator: require('@/assets/img/avator.png'),
      //导入logo图片
      logo: require('@/assets/img/k8s-metrics.png'),
      //控制导航栏折叠
      isCollapse: false,
      //导航栏宽度
      asideWidth: '220px',
      //路由规则
      routers: [],
    }
  },
  computed: {
    //获取用户名
    username() {
      let username = localStorage.getItem('username');
      //三元运算
      return username ? username : '未知';
    },
  },
  methods: {
    //控制折叠
    onCollapse() {
      if (this.isCollapse) {
        this.isCollapse = false
        this.asideWidth = '220px'
      } else {
        this.isCollapse = true
        this.asideWidth = '64px'
      }
    },
    //登出
    logout() {
      //移除用户名
      localStorage.removeItem('username');
      //移除token
      localStorage.removeItem('token');
      //跳转至/login页面
      this.$router.push('/login');
    }
  },
  beforeMount() {
    //使用useRouter().options.routes方法获取路由规则
    this.routers = useRouter().options.routes
  }
}
</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;
  padding-left: 12px;
}
.logo-name{
  font-size: 20px;
  font-weight: bold;
  padding: 10px;
}
/* 滚动条不展示 */
.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 ;
}
</style>

( 4 ) Header
功能:面包屑、下拉框、登出按钮

  1. 面包屑
  2. 展开关闭按钮
  3. 用户信息(退出按钮)

src/layout/Layout.vue

在之前预留header位置填上

        <!-- 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>
                  <!-- this.$route.matched 可以拿到当前页面的路由信息 -->
                  <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>

style追加

 <style scoped> </style>

/* 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;
}

折叠部分是使用改变asideWidth值实现 

  methods: {
    //控制折叠
    onCollapse() {
      if (this.isCollapse) {
        this.isCollapse = false
        this.asideWidth = '220px'
      } else {
        this.isCollapse = true
        this.asideWidth = '64px'
      }
    },

( 5) Main
功能:路由占位符

src/layout/Layout.vue

        <el-main>
          <router-view></router-view>
        </el-main>

 修改

        <el-main class="main">
          <!-- 路由占位符,展示匹配到的路由的视图组件 -->
          <router-view></router-view>
        </el-main>

sytle定义一个内边距

<style scoped>
.main {
  padding: 10px;
}
</style>

( 6 ) Footer

src/layout/Layout.vue

        <el-footer>Footer</el-footer>

在之前预留Footer位置补上

        <!-- footer -->
        <el-footer class="footer">
          <el-icon style="width:2em;top:3px;font-size:18px"><place/></el-icon>
          <a class="footer el-icon-place">Kubernetes 管理系统</a>
        </el-footer>

style

.footer {
  z-index: 1200;
  color: rgb(187, 184, 184);
  font-size: 14px;
  text-align: center;
  line-height: 60px;
}

 Vue前端开发:工作负载

2、工作负载

创建src/workload目录 

2.1 Deployment

src/views/workload/Deployment.vue

<template>
  <div class="home">
    我是Deployment.vue
  </div>
</template>

添加路由规则

 src/router/index.js

    {
        path: '/workload',
        component: Layout,
        icon: "menu", //图标
        meta: {title:"工作负载", requireAuth: false},
        children: [
            {
                path: '/workload/deployment',
                name: 'Deployment',
                icon: "el-icons-s-data", //图标
                meta: {title:"Deployment", requireAuth: true}, //定义meta元数据
                component: () => import('@/views/workload/Deployment.vue')
            },
            {
                path: '/workload/pod',
                name: 'Pod',
                icon: "el-icons-document-add", //图标
                meta: {title:"Pod", requireAuth: true}, //定义meta元数据
                component: () => import('@/views/workload/Pod.vue')
            },
        ]
    },

测试

(1)功能

列表、详情、新增、更新、删除、重启、副本数

src/views/workload/Deployment.vue

<template>
  <div class="deploy">
    <el-row>
      <!-- 头部1 -->
      <el-col :span="24">
        <div>
          <!-- 包一层卡片 -->
          <el-card class="deploy-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <!-- 命名空间的下拉框 -->
              <el-col :span="6">
                <div>
                  <span>命名空间:</span>
                  <!-- 下拉框 -->
                  <!-- filterable:带搜索功能 -->
                  <!-- placeholder 默认提示 -->
                  <!-- label 显示内容 -->
                  <!-- value 绑定到v-model的值中 -->
                  <el-select v-model="namespaceValue" filterable placeholder="请选择" class="deploy-head-card-select">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <!-- 刷新按钮 -->
              <el-col :span="2" :offset="16">
                <div>
                  <!-- 每次刷新,都重新调一次list接口,刷新表格中的数据 -->
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getDeployments()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <!-- 头部2 -->
      <el-col :span="24">
        <div>
          <!-- 包一层卡片 -->
          <el-card class="deploy-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <!-- 创建按钮 -->
              <el-col :span="2">
                <div>
                  <!-- 点击后打开抽屉,填入创建deployment需要的数据 -->
                  <el-button style="border-radius:2px;" icon="Edit" type="primary" @click="createDeploymentDrawer = true" v-loading.fullscreen.lock="fullscreenLoading">创建</el-button>
                </div>
              </el-col>
              <!-- 搜索框和搜索按钮 -->
              <el-col :span="6">
                <div>
                  <el-input class="deploy-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getDeployments()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <!-- 数据表格 -->
      <el-col :span="24">
        <div>
          <!-- 包一层卡片 -->
          <el-card class="deploy-body-card" shadow="never" :body-style="{padding:'5px'}">
            <!-- 数据表格 -->
            <!-- v-loading用于加载时的loading动画 -->
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="deploymentList"
                v-loading="appLoading">
              <!-- 最左侧留出20px的宽度,更加没关 -->
              <el-table-column width="20"></el-table-column>
              <!-- deployment名字 -->
              <el-table-column align=left label="Deployment名">
                <!-- 插槽,scope.row获取当前行的数据 -->
                <template v-slot="scope">
                  <a class="deploy-body-deployname">{
   
   { scope.row.metadata.name }}</a>
                </template>
              </el-table-column>
              <!-- 标签 -->
              <el-table-column align=center label="标签">
                <template v-slot="scope">
                  <!-- for循环,每个label只显示固定长度,鼠标悬停后气泡弹出框显示完整长度 -->
                  <div v-for="(val, key) in scope.row.metadata.labels" :key="key">
                    <!-- 气泡弹出框 -->
                    <!-- placement 弹出位置 -->
                    <!-- trigger 触发条件 -->
                    <!-- content 弹出框内容 -->
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="key + ':' + val">
                      <template #reference>
                        <!-- ellipsis方法用于剪裁字符串 -->
                        <el-tag style="margin-bottom: 5px" type="warning">{
   
   { ellipsis(key + ":" + val) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <!-- 容器组 -->
              <el-table-column align=center label="容器组">
                <!-- 可用数量/总数量,三元运算,若值大于0则显示值,否则显示0 -->
                <template v-slot="scope">
                  <span>{
   
   { scope.row.status.availableReplicas>0?scope.row.status.availableReplicas:0  }} / {
   
   { scope.row.spec.replicas>0?scope.row.spec.replicas:0 }} </span>
                </template>
              </el-table-column>
              <!-- 创建时间 -->
              <el-table-column align=center min-width="100" label="创建时间">
                <!-- timeTrans函数用于将格林威治时间转成北京时间 -->
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
                </template>
              </el-table-column>
              <!-- 容器镜像 -->
              <el-table-column align=center label="镜像">
                <!-- 与label的显示逻辑一致 -->
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.spec.template.spec.containers" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="val.image">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px">{
   
   { ellipsis(val.image.split('/')[2]==undefined?val.image:val.image.split('/')[2]) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <!-- 操作列,放按钮 -->
              <el-table-column align=center label="操作" width="400">
                <template v-slot="scope">
                  <el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getDeploymentDetail(scope)">YAML</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Plus" type="primary" @click="handleScale(scope)">扩缩</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="RefreshLeft" type="primary" @click="handleConfirm(scope, '重启', restartDeployment)">重启</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delDeployment)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <!-- 分页配置 -->
            <!-- background 背景色灰 -->
            <!-- size-change 单页大小改变后触发 -->
            <!-- current-change 页数改变后触发 -->
            <!-- current-page 当前页 -->
            <!-- page-size 单页大小 -->
            <!-- layout 分页器支持的功能 -->
            <!-- total 数据总条数 -->
            <el-pagination
                class="deploy-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                :page-size="pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="deploymentTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <!-- 抽屉:创建Deployment的表单 -->
    <!-- v-model 值是bool,用于显示与隐藏 -->
    <!-- direction 显示的位置 -->
    <!-- before-close 关闭时触发,点击关闭或者点击空白都会触发 -->
    <el-drawer
        v-model="createDeploymentDrawer"
        :direction="direction"
        :before-close="handleClose">
      <!-- 插槽,抽屉标题 -->
      <template #title>
        <h4>创建Deployment</h4>
      </template>
      <!-- 插槽,抽屉body -->
      <template #default>
        <!-- flex布局,居中 -->
        <el-row type="flex" justify="center">
          <el-col :span="20">
            <!-- ref绑定控件后,js中才能用this.$ref获取该控件 -->
            <!-- rules 定义form表单校验规则 -->
            <el-form ref="createDeployment" :rules="createDeploymentRules" :model="createDeployment" label-width="80px">
              <!-- prop用于rules中的校验规则的key -->
              <el-form-item class="deploy-create-form" label="名称" prop="name">
                <el-input v-model="createDeployment.name"></el-input>
              </el-form-item>
              <el-form-item class="deploy-create-form" label="命名空间" prop="namespace">
                <el-select v-model="createDeployment.namespace" filterable placeholder="请选择">
                  <el-option
                      v-for="(item, index) in namespaceList"
                      :key="index"
                      :label="item.metadata.name"
                      :value="item.metadata.name">
                  </el-option>
                </el-select>
              </el-form-item>
              <!-- 数字输入框,最小为1,最大为10 -->
              <el-form-item class="deploy-create-form" label="副本数" prop="replicas">
                <el-input-number v-model="createDeployment.replicas" :min="1" :max="10"></el-input-number>
                <!-- 气泡弹出框用于提醒上限 -->
                <el-popover
                    placement="top"
                    :width="100"
                    trigger="hover"
                    content="申请副本数上限为10个">
                  <template #reference>
                    <el-icon style="width:2em;font-size:18px;color:#4795EE"><WarningFilled/></el-icon>
                  </template>
                </el-popover>
              </el-form-item>
              <el-form-item class="deploy-create-form" label="镜像" prop="image">
                <el-input v-model="createDeployment.image"></el-input>
              </el-form-item>
              <el-form-item class="deploy-create-form" label="标签" prop="label_str">
                <el-input v-model="createDeployment.label_str" placeholder="示例: project=ms,app=gateway"></el-input>
              </el-form-item>
              <!-- 下拉框,用于规格的选择,之后用/分割,得到cpu和内存 -->
              <el-form-item class="deploy-create-form" label="资源配额" prop="resource">
                <el-select v-model="createDeployment.resource" placeholder="请选择">
                  <el-option value="0.5/1" label="0.5C1G"></el-option>
                  <el-option value="1/2" label="1C2G"></el-option>
                  <el-option value="2/4" label="2C4G"></el-option>
                  <el-option value="4/8" label="4C8G"></el-option>
                </el-select>
              </el-form-item>
              <el-form-item class="deploy-create-form" label="容器端口" prop="container_port">
                <el-input v-model="createDeployment.container_port" placeholder="示例: 80"></el-input>
              </el-form-item>
              <el-form-item class="deploy-create-form" label="健康检查" prop="health">
                <el-switch v-model="createDeployment.health_check" />
              </el-form-item>
              <el-form-item class="deploy-create-form" label="检查路径" prop="healthPath">
                <el-input v-model="createDeployment.health_path" placeholder="示例: /health"></el-input>
              </el-form-item>
            </el-form>
          </el-col>
        </el-row>
      </template>
      <!-- 插槽,抽屉footer -->
      <template #footer>
        <!-- 点击后赋值false,隐藏抽屉 -->
        <el-button @click="createDeploymentDrawer = false">取消</el-button>
        <el-button type="primary" @click="submitForm('createDeployment')">立即创建</el-button>
      </template>
    </el-drawer>
    <!-- 展示YAML信息的弹框 -->
    <el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="2%">
      <!-- codemirror编辑器 -->
      <!-- border 带边框 -->
      <!-- options  编辑器配置 -->
      <!-- change 编辑器中的内容变化时触发 -->
      <codemirror
          :value="contentYaml"
          border
          :options="cmOptions"
          height="500"
          style="font-size:14px;"
          @change="onChange"
      ></codemirror>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="this.yamlDialog = false">取 消</el-button>
                    <el-button type="primary" @click="updateDeployment()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
    <!-- 调整副本数的弹框 -->
    <el-dialog title="副本数调整" v-model="scaleDialog" width="25%">
      <div style="text-align:center">
        <span>实例数: </span>
        <el-input-number :step="1" v-model="scaleNum" :min="0" :max="30" label="描述文字"></el-input-number>
      </div>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="scaleDialog = false">取 消</el-button>
                    <el-button type="primary" @click="scaleDeployment()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import common from "../common/Config.js";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
  data() {
    return {
      //编辑器配置
      cmOptions: common.cmOptions,
      contentYaml: '',
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //搜索框内容
      searchInput: '',
      //命名空间
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      //列表
      appLoading: false,
      deploymentList: [],
      deploymentTotal: 0,
      getDeploymentsData: {
        url: common.k8sDeploymentList,
        params: {
          filter_name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //创建
      fullscreenLoading: false,
      direction: 'rtl',
      createDeploymentDrawer: false,
      createDeployment: {
        name: '',
        namespace: '',
        replicas: 1,
        image: '',
        resource: '',
        health_check: false,
        health_path: '',
        label_str: '',
        label: {},
        container_port: ''
      },
      //创建请求的参数
      createDeploymentData: {
        url: common.k8sDeploymentCreate,
        params: {}
      },
      //创建deployment的表单校验规则
      createDeploymentRules: {
        name: [{
          required: true,
          message: '请填写名称',
          trigger: 'change'
        }],
        image: [{
          required: true,
          message: '请填写镜像',
          trigger: 'change'
        }],
        namespace: [{
          required: true,
          message: '请选择命名空间',
          trigger: 'change'
        }],
        resource: [{
          required: true,
          message: '请选择配额',
          trigger: 'change'
        }],
        label_str: [{
          required: true,
          message: '请填写标签',
          trigger: 'change'
        }],
        container_port: [{
          required: true,
          message: '请填写容器端口',
          trigger: 'change'
        }],
      },
      //详情
      deploymentDetail: {},
      getDeploymentDetailData: {
        url: common.k8sDeploymentDetail,
        params: {
          deployment_name: '',
          namespace: ''
        }
      },
      //yaml更新
      yamlDialog: false,
      updateDeploymentData: {
        url: common.k8sDeploymentUpdate,
        params: {
          namespace: '',
          content: ''
        }
      },
      //扩缩容
      scaleNum: 0,
      scaleDialog: false,
      scaleDeploymentData: {
        url: common.k8sDeploymentScale,
        params: {
          deployment_name: '',
          namespace: '',
          scale_num: ''
        }
      },
      //重启
      restartDeploymentData: {
        url: common.k8sDeploymentRestart,
        params: {
          deployment_name: '',
          namespace: '',
        }
      },
      //删除
      delDeploymentData: {
        url: common.k8sDeploymentDel,
        params: {
          deployment_name: '',
          namespace: '',
        }
      },
    }
  },
  methods: {
    //json转yaml方法
    transYaml(content) {
      return json2yaml.stringify(content)
    },
    //yaml转对象
    transObj(content) {
      return yaml2obj.load(content)
    },
    //编辑器内容变化时触发的方式,用于将更新的内容复制到变量中
    onChange(val) {
      this.contentYaml = val
    },
    //页面大小发生变化时触发,赋值并重新获取列表
    handleSizeChange(size) {
      this.pagesize = size;
      this.getDeployments()
    },
    //页数发生变化时触发,复制并重新获取列表
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getDeployments()
    },
    //处理抽屉的关闭,增加体验感
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
    },
    //字符串截取、拼接并返回
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    //格林威治时间转为北京时间
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    //获取Namespace列表
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //获取Deployment列表
    getDeployments() {
      //表格加载动画开启
      this.appLoading = true
      //getDeploymentsData是用于发起deployment列表请求的专用的对象,里面有url和params参数,以下是赋值
      this.getDeploymentsData.params.filter_name = this.searchInput
      this.getDeploymentsData.params.namespace = this.namespaceValue
      this.getDeploymentsData.params.page = this.currentPage
      this.getDeploymentsData.params.limit = this.pagesize
      httpClient.get(this.getDeploymentsData.url, {params: this.getDeploymentsData.params})
          .then(res => {
            //响应成功,获取deployment列表和total
            this.deploymentList = res.data.items
            this.deploymentTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      //加载动画关闭
      this.appLoading = false
    },
    //获取deployment详情,e参数标识传入的scope插槽,.row是该行的数据
    getDeploymentDetail(e) {
      this.getDeploymentDetailData.params.deployment_name = e.row.metadata.name
      this.getDeploymentDetailData.params.namespace = this.namespaceValue
      httpClient.get(this.getDeploymentDetailData.url, {params: this.getDeploymentDetailData.params})
          .then(res => {
            this.contentYaml = this.transYaml(res.data)
            //打开弹出框
            this.yamlDialog = true
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //更新deployment
    updateDeployment() {
      //将yaml格式的deployment对象转为json
      let content = JSON.stringify(this.transObj(this.contentYaml))
      this.updateDeploymentData.params.namespace = this.namespaceValue
      this.updateDeploymentData.params.content = content
      httpClient.put(this.updateDeploymentData.url, this.updateDeploymentData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
            //更新后重新获取列表
            this.getDeployments()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      //关闭弹出框
      this.yamlDialog = false
    },
    //扩缩容的中间方法,用于赋值及打开弹出框
    handleScale(e) {
      this.scaleDialog = true
      this.deploymentDetail = e.row
      this.scaleNum = e.row.spec.replicas
    },
    //扩缩容deployment
    scaleDeployment() {
      this.scaleDeploymentData.params.deployment_name = this.deploymentDetail.metadata.name
      this.scaleDeploymentData.params.namespace = this.namespaceValue
      this.scaleDeploymentData.params.scale_num = this.scaleNum
      httpClient.put(this.scaleDeploymentData.url, this.scaleDeploymentData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
            //更新后重新获取列表
            this.getDeployments()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      //关闭弹出框
      this.scaleDialog = false
    },
    //重启deployment
    restartDeployment(e) {
      this.restartDeploymentData.params.deployment_name = e.row.metadata.name
      this.restartDeploymentData.params.namespace = this.namespaceValue
      httpClient.put(this.restartDeploymentData.url, this.restartDeploymentData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
            this.getDeployments()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //删除deployment
    delDeployment(e) {
      this.delDeploymentData.params.deployment_name = e.row.metadata.name
      this.delDeploymentData.params.namespace = this.namespaceValue
      httpClient.delete(this.delDeploymentData.url, {data: this.delDeploymentData.params})
          .then(res => {
            this.$message.success({
              message: res.msg
            })
            this.getDeployments()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //弹出确认框,用于危险操作的double check
    //obj是行数据,opeateName是操作名,fn是操作的方法
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      //$confirm用于弹出确认框
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
    //创建deployment,加Func的原因是因为createDeploy用于属性了
    createDeployFunc() {
      //正则匹配,验证label的合法性
      let reg = new RegExp("(^[A-Za-z]+=[A-Za-z0-9]+).*")
      if (!reg.test(this.createDeployment.label_str)) {
        this.$message.warning({
          message: "标签填写异常,请确认后重新填写"
        })
        return
      }
      //加载loading动画
      this.fullscreenLoading = true
      //定义label、cpu和memory变量
      let label = new Map()
      let cpu, memory
      //将label字符串转成数组
      let a = (this.createDeployment.label_str).split(",")
      //将数组转成map
      a.forEach(item => {
        let b = item.split("=")
        label[b[0]] = b[1]
      })
      //将deployment的规格转成cpu和memory
      let resourceList = this.createDeployment.resource.split("/")
      cpu = resourceList[0]
      memory = resourceList[1] + "Gi"
      //赋值
      this.createDeploymentData.params = this.createDeployment
      this.createDeploymentData.params.container_port = parseInt(this.createDeployment.container_port)
      this.createDeploymentData.params.label = label
      this.createDeploymentData.params.cpu = cpu
      this.createDeploymentData.params.memory = memory
      httpClient.post(this.createDeploymentData.url, this.createDeploymentData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
            //创建后重新获取列表
            this.getDeployments()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      //重置表单
      this.resetForm('createDeployment')
      //关闭加载动画
      this.fullscreenLoading = false
      //关闭抽屉
      this.createDeploymentDrawer = false
    },
    //重置表单方法,element plus课程讲过的
    resetForm(formName) {
      this.$refs[formName].resetFields()
    },
    //提交表单,校验参数合法性
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.createDeployFunc()
        } else {
          return false;
        }
      })
    }
  },
  watch: {
    //监听namespace的值,若发生变化,则执行handler方法中的内容
    namespaceValue: {
      handler() {
        //将namespace的值存入本地,用于path切换时依旧能获取得到
        localStorage.setItem('namespace', this.namespaceValue)
        //重置当前页为1
        this.currentPage = 1
        //获取deployment列表
        this.getDeployments()
      }
    },
  },
  beforeMount() {
    //加载页面时先获取localStorage中的namespace值,若获取不到则默认default
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getDeployments()
  }
}
</script>


<style scoped>
/* 卡片属性 */
.deploy-head-card,.deploy-body-card {
  border-radius: 5px;
  margin-bottom: 5px;
}
.deploy-head-card-select {
  margin-left: 10px;
}
/* 搜索框 */
.deploy-head-search {
  width:160px;
  margin-right:10px;
}
/* 数据表格deployment名颜色 */
.deploy-body-deployname {
  color: #4795EE;
}
/* deployment名鼠标悬停 */
.deploy-body-deployname:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
</style>

(2)Main布局

(3)引入codemirror编辑器

main.js

//codemirror编辑器
import { GlobalCmComponent } from "codemirror-editor-vue3";
// 引入主题
import 'codemirror/theme/idea.css'
// 引入yaml
import 'codemirror/mode/yaml/yaml.js'

连接配置

src/views/common/Config.js

export default {
    //后端接口路径
    loginAuth: 'http://localhost:9090/api/login',
    k8sWorkflowCreate: 'http://localhost:9090/api/k8s/workflow/create',
    k8sWorkflowDetail: 'http://localhost:9090/api/k8s/workflow/detail',
    k8sWorkflowList: 'http://localhost:9090/api/k8s/workflows',
    k8sWorkflowDel: 'http://localhost:9090/api/k8s/workflow/del',
    k8sDeploymentList: 'http://localhost:9090/api/k8s/deployments',
    k8sDeploymentDetail: 'http://localhost:9090/api/k8s/deployment/detail',
    k8sDeploymentUpdate: 'http://localhost:9090/api/k8s/deployment/update',
    k8sDeploymentScale: 'http://localhost:9090/api/k8s/deployment/scale',
    k8sDeploymentRestart: 'http://localhost:9090/api/k8s/deployment/restart',
    k8sDeploymentDel: 'http://localhost:9090/api/k8s/deployment/del',
    k8sDeploymentCreate: 'http://localhost:9090/api/k8s/deployment/create',
    k8sDeploymentNumNp: 'http://localhost:9090/api/k8s/deployment/numnp',
    k8sPodList: 'http://localhost:9090/api/k8s/pods',
    k8sPodDetail: 'http://localhost:9090/api/k8s/pod/detail',
    k8sPodUpdate: 'http://localhost:9090/api/k8s/pod/update',
    k8sPodDel: 'http://localhost:9090/api/k8s/pod/del',
    k8sPodContainer: 'http://localhost:9090/api/k8s/pod/container',
    k8sPodLog: 'http://localhost:9090/api/k8s/pod/log',
    k8sPodNumNp: 'http://localhost:9090/api/k8s/pod/numnp',
    k8sDaemonSetList: 'http://localhost:9090/api/k8s/daemonsets',
    k8sDaemonSetDetail: 'http://localhost:9090/api/k8s/daemonset/detail',
    k8sDaemonSetUpdate: 'http://localhost:9090/api/k8s/daemonset/update',
    k8sDaemonSetDel: 'http://localhost:9090/api/k8s/daemonset/del',
    k8sStatefulSetList: 'http://localhost:9090/api/k8s/statefulsets',
    k8sStatefulSetDetail: 'http://localhost:9090/api/k8s/statefulset/detail',
    k8sStatefulSetUpdate: 'http://localhost:9090/api/k8s/statefulset/update',
    k8sStatefulSetDel: 'http://localhost:9090/api/k8s/statefulset/del',
    k8sServiceList: 'http://localhost:9090/api/k8s/services',
    k8sServiceDetail: 'http://localhost:9090/api/k8s/service/detail',
    k8sServiceUpdate: 'http://localhost:9090/api/k8s/service/update',
    k8sServiceDel: 'http://localhost:9090/api/k8s/service/del',
    k8sServiceCreate: 'http://localhost:9090/api/k8s/service/create',
    k8sIngressList: 'http://localhost:9090/api/k8s/ingresses',
    k8sIngressDetail: 'http://localhost:9090/api/k8s/ingress/detail',
    k8sIngressUpdate: 'http://localhost:9090/api/k8s/ingress/update',
    k8sIngressDel: 'http://localhost:9090/api/k8s/ingress/del',
    k8sIngressCreate: 'http://localhost:9090/api/k8s/ingress/create',
    k8sConfigMapList: 'http://localhost:9090/api/k8s/configmaps',
    k8sConfigMapDetail: 'http://localhost:9090/api/k8s/configmap/detail',
    k8sConfigMapUpdate: 'http://localhost:9090/api/k8s/configmap/update',
    k8sConfigMapDel: 'http://localhost:9090/api/k8s/configmap/del',
    k8sSecretList: 'http://localhost:9090/api/k8s/secrets',
    k8sSecretDetail: 'http://localhost:9090/api/k8s/secret/detail',
    k8sSecretUpdate: 'http://localhost:9090/api/k8s/secret/update',
    k8sSecretDel: 'http://localhost:9090/api/k8s/secret/del',
    k8sPvcList: 'http://localhost:9090/api/k8s/pvcs',
    k8sPvcDetail: 'http://localhost:9090/api/k8s/pvc/detail',
    k8sPvcUpdate: 'http://localhost:9090/api/k8s/pvc/update',
    k8sPvcDel: 'http://localhost:9090/api/k8s/pvc/del',
    k8sNodeList: 'http://localhost:9090/api/k8s/nodes',
    k8sNodeDetail: 'http://localhost:9090/api/k8s/node/detail',
    k8sNamespaceList: 'http://localhost:9090/api/k8s/namespaces',
    k8sNamespaceDetail: 'http://localhost:9090/api/k8s/namespace/detail',
    k8sNamespaceDel: 'http://localhost:9090/api/k8s/namespace/del',
    k8sPvList: 'http://localhost:9090/api/k8s/pvs',
    k8sPvDetail: 'http://localhost:9090/api/k8s/pv/detail',
    k8sTerminalWs: 'ws://localhost:8081/ws',
    //编辑器配置
    cmOptions: {
        // 语言及语法模式
        mode: 'text/yaml',
        // 主题
        theme: 'idea',
        // 显示行数
        lineNumbers: true,
        smartIndent: true, //智能缩进
        indentUnit: 4, // 智能缩进单元长度为 4 个空格
        styleActiveLine: true, // 显示选中行的样式
        matchBrackets: true, //每当光标位于匹配的方括号旁边时,都会使其高亮显示
        readOnly: false,
        lineWrapping: true //自动换行
    }
}

测试

2.2 Pod

src/views/workload/Pod.vue

<template>
  <div class="home">
    我是Pod.vue
  </div>
</template>

添加路由规则

(1)功能

 列表、详情、更新、删除、日志、终端

(2)布局

(3)头部工具栏

(4)数据表格

展开Expand

容器

日志

<template>
  <div class="pod">
    <el-row>
      <!-- 头部1 -->
      <el-col :span="24">
        <div>
          <el-card class="pod-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="6">
                <div>
                  <span>命名空间: </span>
                  <el-select v-model="namespaceValue" filterable placeholder="请选择">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <el-col :span="2" :offset="16">
                <div>
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getPods()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <!-- 头部2 -->
      <el-col :span="24">
        <div>
          <el-card class="pod-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="2">
                <div>
                  <el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
                </div>
              </el-col>
              <el-col :span="6">
                <div>
                  <el-input class="pod-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getPods()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <!-- 数据表格 -->
      <el-col :span="24">
        <div>
          <el-card class="pod-body-card" shadow="never" :body-style="{padding:'5px'}">
            <!-- 数据表格 -->
            <!-- row-key 用来定义行数据的key,结合expand-row-keys使用,往expandKeys中增加key来展开行 -->
            <!-- expand-row-keys 展开的行的key数组 -->
            <!-- expand-change 展开触发时,调用这个方法 -->
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="podList"
                v-loading="appLoading"
                :row-key="getRowKeys"
                :expand-row-keys="expandKeys"
                @expand-change="expandChange">
              <el-table-column width="10"></el-table-column>
              <!-- 展开 -->
              <el-table-column type="expand">
                <!-- 插槽,里面是展开的内容,props标识展开的行的数据 -->
                <template #default="props">
                  <el-tabs v-model="activeName" type="card">
                    <!-- tab容器标签页 -->
                    <el-tab-pane label="容器" name="container">
                      <el-card shadow="never" style="border-radius:1px;" :body-style="{padding:'5px'}">
                        <!-- 嵌套数据表格 -->
                        <el-table
                            style="width:100%;font-size:12px;"
                            :data="props.row.spec.containers">
                          <el-table-column align=left prop="name" label="容器名"></el-table-column>
                          <el-table-column align=left prop="image" label="镜像"></el-table-column>
                          <el-table-column align=center label="Pod IP">
                            <span>{
   
   { props.row.status.podIP }}</span>
                          </el-table-column>
                          <el-table-column align=center prop="args" label="启动命令"></el-table-column>
                          <el-table-column align=center label="环境变量">
                            <template v-slot="scope">
                              <!-- 气泡弹出框,内容是所有的环境变量 -->
                              <el-popover :width="500" placement="left" trigger="hover">
                                <el-table style="width:100%;font-size:12px;" size="mini" :show-header="false" :data="scope.row.env">
                                  <el-table-column property="name" label="名称"></el-table-column>
                                  <el-table-column property="value" label="值"></el-table-column>
                                </el-table>
                                <template #reference>
                                  <el-button size="small">此处查看</el-button>
                                </template>
                              </el-popover>
                            </template>
                          </el-table-column>
                        </el-table>
                      </el-card>
                    </el-tab-pane>
                    <!-- tab日志标签页 -->
                    <el-tab-pane label="日志" name="log">
                      <el-card shadow="never" style="border-radius:1px;" :body-style="{padding:'5px'}">
                        <el-row :gutter="10">
                          <el-col :span="3">
                            <!-- 容器选择框 -->
                            <el-select size="small" v-model="containerValue" placeholder="请选择">
                              <el-option v-for="item in containerList" :key="item" :value="item">
                              </el-option>
                            </el-select>
                          </el-col>
                          <el-col :span="2">
                            <!-- 查看日志按钮 -->
                            <el-button style="border-radius:2px;" size="small" type="primary" @click="getPodLog(props.row.metadata.name)">查看</el-button>
                          </el-col>
                          <el-col :span="24" style="margin-top: 5px">
                            <!-- 显示日志内容 -->
                            <el-card shadow="never" class="pod-body-log-card" :body-style="{padding:'5px'}">
                              <span class="pod-body-log-span">{
   
   { logContent }}</span>
                            </el-card>
                          </el-col>
                        </el-row>
                      </el-card>
                    </el-tab-pane>
                    <!-- tab终端标签页 -->
                    <el-tab-pane label="终端" name="shell">
                      <el-card shadow="never" style="border-radius:1px;" :body-style="{padding:'5px'}">
                        <el-row :gutter="10">
                          <el-col :span="3">
                            <!-- 容器选择框 -->
                            <el-select size="small" v-model="containerValue" placeholder="请选择">
                              <el-option v-for="item in containerList" :key="item" :value="item">
                              </el-option>
                            </el-select>
                          </el-col>
                          <el-col :span="1">
                            <!-- 连接按钮 -->
                            <el-button style="border-radius:2px;" size="small" type="primary" @click="initSocket(props.row)">连接</el-button>
                          </el-col>
                          <el-col :span="1">
                            <!-- 关闭连接按钮 -->
                            <el-button style="border-radius:2px;" size="small" type="danger" @click="closeSocket()">关闭</el-button>
                          </el-col>
                          <el-col :span="24" style="margin-top: 5px">
                            <el-card shadow="never" class="pod-body-shell-card" :body-style="{padding:'5px'}">
                              <!-- xterm虚拟终端 -->
                              <div id="xterm"></div>
                            </el-card>
                          </el-col>
                        </el-row>
                      </el-card>
                    </el-tab-pane>
                  </el-tabs>
                </template>
              </el-table-column>
              <el-table-column align=left label="Pod名">
                <template v-slot="scope">
                  <!-- 三元运算:expandMap[scope.row.metadata.name]为1则
                  触发关闭(expandedRows为空数组),为0则触发展开expandedRows有值 -->
                  <a class="pod-body-podname" @click="expandMap[scope.row.metadata.name] ? expandChange(scope.row, []) : expandChange(scope.row, [scope.row])">{
   
   { scope.row.metadata.name }}</a>
                </template>
              </el-table-column>
              <el-table-column align=center min-width="150" label="节点">
                <template v-slot="scope">
                  <el-tag v-if="scope.row.spec.nodeName !== undefined" type="warning">{
   
   { scope.row.spec.nodeName }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="状态">
                <template v-slot="scope">
                  <div :class="{'success-dot':scope.row.status.phase == 'Running' || scope.row.status.phase == 'Succeeded', 'warning-dot':scope.row.status.phase == 'Pending', 'error-dot':scope.row.status.phase != 'Running' && scope.row.status.phase != 'Pending' && scope.row.status.phase != 'Succeeded'}"></div>
                  <span :class="{'success-status':scope.row.status.phase == 'Running' || scope.row.status.phase == 'Succeeded', 'warning-status':scope.row.status.phase == 'Pending', 'error-status':scope.row.status.phase != 'Running' && scope.row.status.phase != 'Pending' && scope.row.status.phase != 'Succeeded'}">{
   
   { scope.row.status.phase }} </span>
                </template>
              </el-table-column>
              <el-table-column align=center label="重启数">
                <template v-slot="scope">
                  <span>{
   
   { restartTotal(scope) }} </span>
                </template>
              </el-table-column>
              <el-table-column align=center min-width="100" label="创建时间">
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="操作" width="200">
                <template v-slot="scope">
                  <el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getPodDetail(scope)">YAML</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delPod)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-pagination
                class="pod-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                :page-size="pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="podTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <!-- 展示YAML信息的弹框 -->
    <el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
      <codemirror
          :value="contentYaml"
          border
          :options="cmOptions"
          height="500"
          style="font-size:14px;"
          @change="onChange"
      ></codemirror>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="yamlDialog = false">取 消</el-button>
                    <el-button type="primary" @click="updatePod()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
//引入xterm终端依赖
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import 'xterm/lib/xterm.js';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
  data() {
    return {
      //编辑器配置
      cmOptions: common.cmOptions,
      contentYaml: '',
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //
      searchInput: '',
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      appLoading: false,
      podList: [],
      podTotal: 0,
      getPodsData: {
        url: common.k8sPodList,
        params: {
          filter_name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //详情
      podDetail: {},
      getPodDetailData: {
        url: common.k8sPodDetail,
        params: {
          pod_name: '',
          namespace: ''
        }
      },
      //yaml更新
      yamlDialog: false,
      updatePodData: {
        url: common.k8sPodUpdate,
        params: {
          namespace: '',
          content: ''
        }
      },
      //删除
      delPodData: {
        url: common.k8sPodDel,
        params: {
          pod_name: '',
          namespace: ''
        }
      },
      //expand扩展
      activeName: 'container',
      expandKeys: [],
      expandMap: {},
      //日志
      containerList: {},
      containerValue: '',
      getPodContainerData: {
        url: common.k8sPodContainer,
        params: {
          pod_name: '',
          namespace: ''
        }
      },
      logContent: '',
      getPodLogData: {
        url: common.k8sPodLog,
        params: {
          container_name: '',
          pod_name: '',
          namespace: ''
        }
      },
      //terminal
      term: null,
      socket: null
    }
  },
  methods: {
    transYaml(content) {
      return json2yaml.stringify(content)
    },
    transObj(content) {
      return yaml2obj.load(content)
    },
    onChange(val) {
      this.contentYaml = val
    },
    handleSizeChange(size) {
      this.pagesize = size;
      this.getPods()
    },
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getPods()
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
    },
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    restartTotal(e) {
      let index, sum = 0
      let containerStatuses = e.row.status.containerStatuses
      for ( index in containerStatuses) {
        sum = sum + containerStatuses[index].restartCount
      }
      return sum
    },
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getPods() {
      this.appLoading = true
      this.getPodsData.params.filter_name = this.searchInput
      this.getPodsData.params.namespace = this.namespaceValue
      this.getPodsData.params.page = this.currentPage
      this.getPodsData.params.limit = this.pagesize
      httpClient.get(this.getPodsData.url, {params: this.getPodsData.params})
          .then(res => {
            this.podList = res.data.items
            this.podTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.appLoading = false
    },
    getPodDetail(e) {
      this.getPodDetailData.params.pod_name = e.row.metadata.name
      this.getPodDetailData.params.namespace = this.namespaceValue
      httpClient.get(this.getPodDetailData.url, {params: this.getPodDetailData.params})
          .then(res => {
            this.podDetail = res.data
            this.contentYaml = this.transYaml(this.podDetail)
            this.yamlDialog = true
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    updatePod() {
      let content = JSON.stringify(this.transObj(this.contentYaml))
      this.updatePodData.params.namespace = this.namespaceValue
      this.updatePodData.params.content = content
      httpClient.put(this.updatePodData.url, this.updatePodData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.yamlDialog = false
    },
    delPod(e) {
      this.delPodData.params.pod_name = e.row.metadata.name
      this.delPodData.params.namespace = this.namespaceValue
      httpClient.delete(this.delPodData.url, {data: this.delPodData.params})
          .then(res => {
            this.getPods()
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
    getRowKeys(row) {
      return row.metadata.name
    },
    //row,展开的当前行的数据
    //expandedRows,展开的所有行的数据组成的数组,但是这里用法是只会有一行,也就是数组长度永远为1
    expandChange(row, expandedRows) {
      //初始化变量
      //清空expandKeys,代表关闭所有展开的行
      this.expandKeys = []
      //清空日志内容
      this.logContent= ''
      //清空containervalue,展开时不显示上次的值
      this.containerValue = ''
      //将tab标签页顶部页面调成容器
      this.activeName = 'container'
      //expandedRows.length == 1表示展开,expandedRows.length == 0 表示关闭
      if (expandedRows.length > 0) {
        //expandMap key表示展开过的行的key,值为1表示展开标记,值为0表示关闭标记
        //expandMap用于数据表格点击name的展开,用于判断这一行是展开还是关闭的行为
        this.expandMap[row.metadata.name] = 1
        //将expandMap除了row.metadata.name,其他key的值都置为0
        this.setExpandMap(row.metadata.name)
        //这里才是真正的展开,将row.metadata.name添加到expandKeys数组中展开,然后执行方法获取container
        this.expandKeys.push(row.metadata.name)
        this. getPodContainer(row)
      } else {
        //关闭标记
        this.expandMap[row.metadata.name] = 0
      }
    },
    //匹配expandMap中podName,不相等的全都置为0,意为除了podName这行,其他全都标记关闭
    setExpandMap(podName) {
      let key
      for ( key in this.expandMap ) {
        key !== podName ? this.expandMap[key] = 0 : ''
      }
    },
    getPodContainer(row) {
      this.getPodContainerData.params.pod_name = row.metadata.name
      this.getPodContainerData.params.namespace = this.namespaceValue
      httpClient.get(this.getPodContainerData.url, {params: this.getPodContainerData.params})
          .then(res => {
            this.containerList = res.data
            this.containerValue = this.containerList[0]
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getPodLog(podName) {
      this.getPodLogData.params.pod_name = podName
      this.getPodLogData.params.container_name = this.containerValue
      this.getPodLogData.params.namespace = this.namespaceValue
      httpClient.get(this.getPodLogData.url, {params: this.getPodLogData.params})
          .then(res => {
            this.logContent = res.data
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    initTerm() {
      //初始化xterm实例
      this.term = new Terminal({
        rendererType: 'canvas', //渲染类型
        rows: 30, //行数
        cols: 110,
        convertEol: false, //启用时,光标将设置为下一行的开头
        scrollback: 10, //终端中的回滚量
        disableStdin: false, //是否应禁用输入
        cursorStyle: 'underline', //光标样式
        cursorBlink: true, //光标闪烁
        theme: {
          foreground: 'white', //字体
          background: '#060101', //背景色
          cursor: 'help' //设置光标
        }
      });
      //绑定dom
      this.term.open(document.getElementById('xterm'))
      //终端适应父元素大小
      const fitAddon = new FitAddon()
      this.term.loadAddon(fitAddon)
      fitAddon.fit();
      //获取终端的焦点
      this.term.focus();
      let _this = this; //一定要重新定义一个this,不然this指向会出问题
      //onData方法用于定义输入的动作
      this.term.onData(function (key) {
        // 这里key值是输入的值,数据格式就是后端定义的 {"operation":"stdin","data":"ls"}
        let msgOrder = {
          operation: 'stdin',
          data: key,
        };
        //发送数据
        _this.socket.send(JSON.stringify(msgOrder));
      });
      //发送resize请求
      let msgOrder2 = {
        operation: 'resize',
        cols: this.term.cols,
        rows: this.term.rows,
      };
      this.socket.send(JSON.stringify(msgOrder2))
    },
    //初始化websocket
    initSocket(row) {
      //定义websocket连接地址
      let terminalWsUrl = common.k8sTerminalWs + "?pod_name=" + row.metadata.name + "&container_name=" + this.containerValue + "&namespace=" + this.namespaceValue
      //实例化
      this.socket = new WebSocket(terminalWsUrl);
      //关闭连接时的方法
      this.socketOnClose();
      //建立连接时的方法
      this.socketOnOpen();
      //接收消息的方法
      this.socketOnMessage();
      //报错时的方法
      this.socketOnError();
    },
    socketOnOpen() {
      this.socket.onopen = () => {
        //简历连接成功后,初始化虚拟终端
        this.initTerm()
      }
    },
    socketOnMessage() {
      this.socket.onmessage = (msg) => {
        //接收到消息后将字符串转为对象,输出data内容
        let content = JSON.parse(msg.data)
        this.term.write(content.data)
      }
    },
    socketOnClose() {
      this.socket.onclose = () => {
        //关闭连接后打印在终端里
        this.term.write("链接已关闭")
      }
    },
    socketOnError() {
      this.socket.onerror = () => {
        console.log('socket 链接失败')
      }
    },
    //关闭连接
    closeSocket() {
      //若没有实例化,则不需要关闭
      if (this.socket === null) {
        return
      }
      this.term.write("链接关闭中。。。")
      this.socket.close()
    }
  },
  watch: {
    namespaceValue: {
      handler() {
        localStorage.setItem('namespace', this.namespaceValue)
        this.currentPage = 1
        this.getPods()
      }
    },
    //若tab标签页切到日志,则重新加载日志内容
    activeName: {
      handler() {
        if ( this.activeName == 'log' ) {
          this.expandKeys.length == 1 ? this.getPodLog(this.expandKeys[0]) : ''
        }
      }
    }
  },
  beforeMount() {
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getPods()
  },
  beforeUnmount() {
    //若websocket连接没有关闭,则在改生命周期关闭
    if ( this.socket !== null ) {
      this.socket.close()
    }
  },
}
</script>


<style scoped>
.pod-head-card,.pod-body-card {
  border-radius: 1px;
  margin-bottom: 5px;
}
.pod-head-search {
  width:160px;
  margin-right:10px;
}
.pod-body-podname {
  color: #4795EE;
}
.pod-body-podname:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
/* pod状态栏圆点的css实现 */
.success-dot{
  display:inline-block;
  width: 7px;
  height:7px;
  background: rgb(27, 202, 21);
  border-radius:50%;
  border:1px solid rgb(27, 202, 21);
  margin-right: 10px;
}
.warning-dot{
  display:inline-block;
  width: 7px;
  height:7px;
  background: rgb(233, 200, 16);
  border-radius:50%;
  border:1px solid rgb(233, 200, 16);
  margin-right: 10px;
}
.error-dot{
  display:inline-block;
  width: 7px;
  height:7px;
  background: rgb(226, 23, 23);
  border-radius:50%;
  border:1px solid rgb(226, 23, 23);
  margin-right: 10px;
}
.success-status {
  color: rgb(27, 202, 21);
}
.warning-status {
  color: rgb(233, 200, 16);
}
.error-status {
  color: rgb(226, 23, 23);
}
/deep/ .el-tabs__item {
  font-size: 12px;
}
/deep/ .el-tabs__header {
  margin-bottom: 8px;
}
.pod-body-log-card, .pod-body-shell-card {
  border-radius:1px;
  height:600px;
  overflow:auto;
  background-color: #060101;
}
.pod-body-log-card {
  color: aliceblue;
}
.pod-body-log-span {
  white-space:pre;
}
</style>

测试 

2.3 DaemonSet

 src/views/workload/DaemonSet.vue

<template>
  <div class="daemonset">
    <el-row>
      <el-col :span="24">
        <div>
          <el-card class="daemonset-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="6">
                <div>
                  <span>命名空间: </span>
                  <el-select v-model="namespaceValue" filterable placeholder="请选择">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <el-col :span="2" :offset="16">
                <div>
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getDaemonSets()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="daemonset-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="2">
                <div>
                  <el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
                </div>
              </el-col>
              <el-col :span="6">
                <div>
                  <el-input class="daemonset-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getDaemonSets()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="daemonset-body-card" shadow="never" :body-style="{padding:'5px'}">
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="daemonSetList"
                v-loading="appLoading">
              <el-table-column width="20"></el-table-column>
              <el-table-column align=left label="DaemonSet名">
                <template v-slot="scope">
                  <a class="daemonset-body-daemonsetname">{
   
   { scope.row.metadata.name }}</a>
                </template>
              </el-table-column>
              <el-table-column align=center label="标签">
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.metadata.labels" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="key + ':' + val">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px" type="warning">{
   
   { ellipsis(key + ":" + val) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <el-table-column align=center label="容器组">
                <template v-slot="scope">
                  <span>{
   
   { scope.row.status.numberAvailable>0?scope.row.status.numberAvailable:0  }} / {
   
   { scope.row.status.desiredNumberScheduled>0?scope.row.status.desiredNumberScheduled:0 }} </span>
                </template>
              </el-table-column>
              <el-table-column align=center min-width="100" label="创建时间">
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="镜像">
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.spec.template.spec.containers" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="val.image">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px">{
   
   { ellipsis(val.image.split('/')[2]==undefined?val.image:val.image.split('/')[2]) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <el-table-column align=center label="操作" width="200">
                <template v-slot="scope">
                  <el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getDaemonSetDetail(scope)">YAML</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delDaemonSet)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-pagination
                class="daemonset-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                :page-size="pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="daemonSetTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
      <codemirror
          :value="contentYaml"
          border
          :options="cmOptions"
          height="500"
          style="font-size:14px;"
          @change="onChange"
      ></codemirror>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="yamlDialog = false">取 消</el-button>
                    <el-button type="primary" @click="updateDaemonSet()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
  data() {
    return {
      //编辑器配置
      cmOptions: common.cmOptions,
      contentYaml: '',
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //
      searchInput: '',
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      appLoading: false,
      daemonSetList: [],
      daemonSetTotal: 0,
      getDaemonSetsData: {
        url: common.k8sDaemonSetList,
        params: {
          filter_name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //详情
      daemonSetDetail: {},
      getDaemonSetDetailData: {
        url: common.k8sDaemonSetDetail,
        params: {
          daemonset_name: '',
          namespace: ''
        }
      },
      //yaml更新
      yamlDialog: false,
      updateDaemonSetData: {
        url: common.k8sDaemonSetUpdate,
        params: {
          namespace: '',
          content: ''
        }
      },
      //删除
      delDaemonSetData: {
        url: common.k8sDaemonSetDel,
        params: {
          daemonset_name: '',
          namespace: '',
        }
      }
    }
  },
  methods: {
    transYaml(content) {
      return json2yaml.stringify(content)
    },
    transObj(content) {
      return yaml2obj.load(content)
    },
    onChange(val) {
      this.contentYaml = val
    },
    handleSizeChange(size) {
      this.pagesize = size;
      this.getDaemonSets()
    },
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getDaemonSets()
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
    },
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    restartTotal(e) {
      let index, sum = 0
      let containerStatuses = e.row.status.containerStatuses
      for ( index in containerStatuses) {
        sum = sum + containerStatuses[index].restartCount
      }
      return sum
    },
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getDaemonSets() {
      this.appLoading = true
      this.getDaemonSetsData.params.filter_name = this.searchInput
      this.getDaemonSetsData.params.namespace = this.namespaceValue
      this.getDaemonSetsData.params.page = this.currentPage
      this.getDaemonSetsData.params.limit = this.pagesize
      httpClient.get(this.getDaemonSetsData.url, {params: this.getDaemonSetsData.params})
          .then(res => {
            this.daemonSetList = res.data.items
            this.daemonSetTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.appLoading = false
    },
    getDaemonSetDetail(e) {
      this.getDaemonSetDetailData.params.daemonset_name = e.row.metadata.name
      this.getDaemonSetDetailData.params.namespace = this.namespaceValue
      httpClient.get(this.getDaemonSetDetailData.url, {params: this.getDaemonSetDetailData.params})
          .then(res => {
            this.daemonSetDetail = res.data
            this.contentYaml = this.transYaml(this.daemonSetDetail)
            this.yamlDialog = true
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    updateDaemonSet() {
      let content = JSON.stringify(this.transObj(this.contentYaml))
      this.updateDaemonSetData.params.namespace = this.namespaceValue
      this.updateDaemonSetData.params.content = content
      httpClient.put(this.updateDaemonSetData.url, this.updateDaemonSetData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.yamlDialog = false
    },
    delDaemonSet(e) {
      this.delDaemonSetData.params.daemonset_name = e.row.metadata.name
      this.delDaemonSetData.params.namespace = this.namespaceValue
      httpClient.delete(this.delDaemonSetData.url, {data: this.delDaemonSetData.params})
          .then(res => {
            this.getDaemonSets()
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
  },
  watch: {
    namespaceValue: {
      handler() {
        localStorage.setItem('namespace', this.namespaceValue)
        this.currentPage = 1
        this.getDaemonSets()
      }
    },
  },
  beforeMount() {
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getDaemonSets()
  }
}
</script>


<style scoped>
.daemonset-head-card,.daemonset-body-card {
  border-radius: 1px;
  margin-bottom: 5px;
}
.daemonset-head-search {
  width:160px;
  margin-right:10px;
}
.daemonset-body-daemonsetname {
  color: #4795EE;
}
.daemonset-body-daemonsetname:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
</style>

添加路由

src/router/index.js

            {
                path: '/workload/daemonset',
                name: 'DaemonSet',
                icon: "el-icons-document-add", //图标
                meta: {title:"Pod", requireAuth: true}, //定义meta元数据
                component: () => import('@/views/workload/DaemonSet.vue')
            },

daemonset.yaml

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      tolerations:
      # 这些容忍度设置是为了让该守护进程集在控制平面节点上运行
      # 如果你不希望自己的控制平面节点运行 Pod,可以删除它们
      - key: node-role.kubernetes.io/control-plane
        operator: Exists
        effect: NoSchedule
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      containers:
      - name: fluentd-elasticsearch
        image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log

先创建kubectl apply -f  daemonset.yaml

然后测试

2.4 StatefulSet

src/views/workload/StatefulSet.vue

<template>
  <div class="statefulset">
    <el-row>
      <el-col :span="24">
        <div>
          <el-card class="statefulset-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="6">
                <div>
                  <span>命名空间: </span>
                  <el-select v-model="namespaceValue" filterable placeholder="请选择">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <el-col :span="2" :offset="16">
                <div>
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getStatefulSets()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="statefulset-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="2">
                <div>
                  <el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
                </div>
              </el-col>
              <el-col :span="6">
                <div>
                  <el-input class="statefulset-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getStatefulSets()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="statefulset-body-card" shadow="never" :body-style="{padding:'5px'}">
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="statefulSetList"
                v-loading="appLoading">
              <el-table-column width="20"></el-table-column>
              <el-table-column align=left label="StatefulSet名">
                <template v-slot="scope">
                  <a class="statefulset-body-statefulsetname">{
   
   { scope.row.metadata.name }}</a>
                </template>
              </el-table-column>
              <el-table-column align=center label="标签">
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.metadata.labels" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="key + ':' + val">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px" type="warning">{
   
   { ellipsis(key + ":" + val) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <el-table-column align=center label="容器组">
                <template v-slot="scope">
                  <span>{
   
   { scope.row.status.currentReplicas>0?scope.row.status.currentReplicas:0  }} / {
   
   { scope.row.spec.replicas>0?scope.row.spec.replicas:0 }} </span>
                </template>
              </el-table-column>
              <el-table-column align=center min-width="100" label="创建时间">
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="镜像">
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.spec.template.spec.containers" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="val.image">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px">{
   
   { ellipsis(val.image.split('/')[2]==undefined?val.image:val.image.split('/')[2]) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <el-table-column align=center label="操作" width="200">
                <template v-slot="scope">
                  <el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getStatefulSetDetail(scope)">YAML</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delStatefulSet)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-pagination
                class="statefulset-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                :page-size="pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="statefulSetTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
      <codemirror
          :value="contentYaml"
          border
          :options="cmOptions"
          height="500"
          style="font-size:14px;"
          @change="onChange"
      ></codemirror>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="yamlDialog = false">取 消</el-button>
                    <el-button type="primary" @click="updateStatefulSet()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
  data() {
    return {
      //编辑器配置
      cmOptions: common.cmOptions,
      contentYaml: '',
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //
      searchInput: '',
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      appLoading: false,
      statefulSetList: [],
      statefulSetTotal: 0,
      getStatefulSetsData: {
        url: common.k8sStatefulSetList,
        params: {
          filter_name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //详情
      statefulSetDetail: {},
      getStatefulSetDetailData: {
        url: common.k8sStatefulSetDetail,
        params: {
          statefulset_name: '',
          namespace: ''
        }
      },
      //yaml更新
      yamlDialog: false,
      updateStatefulSetData: {
        url: common.k8sStatefulSetUpdate,
        params: {
          namespace: '',
          content: ''
        }
      },
      //删除
      delStatefulSetData: {
        url: common.k8sStatefulSetDel,
        params: {
          statefulset_name: '',
          namespace: '',
        }
      }
    }
  },
  methods: {
    transYaml(content) {
      return json2yaml.stringify(content)
    },
    transObj(content) {
      return yaml2obj.load(content)
    },
    onChange(val) {
      this.contentYaml = val
    },
    handleSizeChange(size) {
      this.pagesize = size;
      this.getStatefulSets()
    },
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getStatefulSets()
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
    },
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    restartTotal(e) {
      let index, sum = 0
      let containerStatuses = e.row.status.containerStatuses
      for ( index in containerStatuses) {
        sum = sum + containerStatuses[index].restartCount
      }
      return sum
    },
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getStatefulSets() {
      this.appLoading = true
      this.getStatefulSetsData.params.filter_name = this.searchInput
      this.getStatefulSetsData.params.namespace = this.namespaceValue
      this.getStatefulSetsData.params.page = this.currentPage
      this.getStatefulSetsData.params.limit = this.pagesize
      httpClient.get(this.getStatefulSetsData.url, {params: this.getStatefulSetsData.params})
          .then(res => {
            this.statefulSetList = res.data.items
            this.statefulSetTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.appLoading = false
    },
    getStatefulSetDetail(e) {
      this.getStatefulSetDetailData.params.statefulset_name = e.row.metadata.name
      this.getStatefulSetDetailData.params.namespace = this.namespaceValue
      httpClient.get(this.getStatefulSetDetailData.url, {params: this.getStatefulSetDetailData.params})
          .then(res => {
            this.statefulSetDetail = res.data
            this.contentYaml = this.transYaml(this.statefulSetDetail)
            this.yamlDialog = true
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    updateStatefulSet() {
      let content = JSON.stringify(this.transObj(this.contentYaml))
      this.updateStatefulSetData.params.namespace = this.namespaceValue
      this.updateStatefulSetData.params.content = content
      httpClient.put(this.updateStatefulSetData.url, this.updateStatefulSetData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.yamlDialog = false
    },
    delStatefulSet(e) {
      this.delStatefulSetData.params.statefulset_name = e.row.metadata.name
      this.delStatefulSetData.params.namespace = this.namespaceValue
      httpClient.delete(this.delStatefulSetData.url, {data: this.delStatefulSetData.params})
          .then(res => {
            this.getStatefulSets()
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
  },
  watch: {
    namespaceValue: {
      handler() {
        localStorage.setItem('namespace', this.namespaceValue)
        this.currentPage = 1
        this.getStatefulSets()
      }
    },
  },
  beforeMount() {
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getStatefulSets()
  }
}
</script>


<style scoped>
.statefulset-head-card,.statefulset-body-card {
  border-radius: 1px;
  margin-bottom: 5px;
}
.statefulset-head-search {
  width:160px;
  margin-right:10px;
}
.statefulset-body-statefulsetname {
  color: #4795EE;
}
.statefulset-body-statefulsetname:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
</style>

 添加路由

/workload路由下添加子路由:

先创建statefulset.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: ngin
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx # 必须匹配 .spec.template.metadata.labels
  serviceName: "nginx"
  replicas: 2 # 默认值是 1
  #minReadySeconds: 10 # 默认值是 0
  template:
    metadata:
      labels:
        app: nginx # 必须匹配 .spec.selector.matchLabels
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
          name: web

 kubectl apply -f statefulset.yaml

然后测试

3、集群

3.1 Node 

3.2 Namespace

3.3 PV

4、负载均衡

4.1 Service

 src/views/loadbalance/Service.vue

<template>
  <div class="service">
    <el-row>
      <el-col :span="24">
        <div>
          <el-card class="service-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="6">
                <div>
                  <span>命名空间: </span>
                  <el-select v-model="namespaceValue" filterable placeholder="请选择">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <el-col :span="2" :offset="16">
                <div>
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getServices()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="service-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="2">
                <div>
                  <el-button style="border-radius:2px;" icon="Edit" type="primary" @click="createServiceDrawer = true" v-loading.fullscreen.lock="fullscreenLoading">创建</el-button>
                </div>
              </el-col>
              <el-col :span="6">
                <div>
                  <el-input class="service-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getServices()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="service-body-card" shadow="never" :body-style="{padding:'5px'}">
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="serviceList"
                v-loading="appLoading">
              <el-table-column width="20"></el-table-column>
              <el-table-column align=left label="Service名">
                <template v-slot="scope">
                  <a class="service-body-servicename">{
   
   { scope.row.metadata.name }}</a>
                </template>
              </el-table-column>
              <el-table-column align=center label="标签" min-width='120'>
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.metadata.labels" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="key + ':' + val">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px" type="warning">{
   
   { ellipsis(key + ":" + val) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <el-table-column align=center label="类型">
                <template v-slot="scope">
                  <span style="font-weight:bold;">{
   
   { scope.row.spec.type }} </span>
                </template>
              </el-table-column>
              <el-table-column align=center label="CLUSTER-IP">
                <template v-slot="scope">
                  <span>{
   
   { scope.row.spec.clusterIP }} </span>
                </template>
              </el-table-column>
              <el-table-column align=center label="EXTERNAL-IP">
                <template v-slot="scope">
                  <span>{
   
   { scope.row.status.loadBalancer.ingress ? scope.row.status.loadBalancer.ingress[0].ip : '' }} </span>
                </template>
              </el-table-column>
              <el-table-column align=center label="端口">
                <template v-slot="scope">
                  <span v-if="!scope.row.spec.ports[0].nodePort">{
   
   { scope.row.spec.ports[0].port }}/{
   
   { scope.row.spec.ports[0].protocol }}</span>
                  <span v-if="scope.row.spec.ports[0].nodePort">{
   
   { scope.row.spec.ports[0].port }}:{
   
   { scope.row.spec.ports[0].nodePort }}/{
   
   { scope.row.spec.ports[0].protocol }}</span>
                </template>
              </el-table-column>
              <el-table-column align=center min-width="100" label="创建时间">
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="操作" width="200">
                <template v-slot="scope">
                  <el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getServiceDetail(scope)">YAML</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delService)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-pagination
                class="service-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                :page-size="pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="serviceTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
      <codemirror
          :value="contentYaml"
          border
          :options="cmOptions"
          height="500"
          style="font-size:14px;"
          @change="onChange"
      ></codemirror>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="yamlDialog = false">取 消</el-button>
                    <el-button type="primary" @click="updateService()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
    <el-drawer
        v-model="createServiceDrawer"
        :direction="direction"
        :before-close="handleClose">
      <template #title>
        <h4>创建Service</h4>
      </template>
      <template #default>
        <el-row type="flex" justify="center">
          <el-col :span="20">
            <el-form ref="createService" :rules="createServiceRules" :model="createService" label-width="80px">
              <el-form-item class="service-create-form" label="名称" prop="name">
                <el-input v-model="createService.name"></el-input>
              </el-form-item>
              <el-form-item class="service-create-form" label="命名空间" prop="namespace">
                <el-select v-model="createService.namespace" filterable placeholder="请选择">
                  <el-option
                      v-for="(item, index) in namespaceList"
                      :key="index"
                      :label="item.metadata.name"
                      :value="item.metadata.name">
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item class="service-create-form" label="类型" prop="type">
                <el-select v-model="createService.type" placeholder="请选择">
                  <el-option value="ClusterIP" label="ClusterIP"></el-option>
                  <el-option value="NodePort" label="NodePort"></el-option>
                </el-select>
              </el-form-item>
              <el-form-item class="deploy-create-form" label="容器端口" prop="container_port">
                <el-input v-model="createService.container_port" placeholder="示例: 80"></el-input>
              </el-form-item>
              <el-form-item class="service-create-form" label="Service端口" prop="port">
                <el-input v-model="createService.port" placeholder="示例: 80"></el-input>
              </el-form-item>
              <el-form-item v-if="createService.type == 'NodePort'" class="service-create-form" label="NodePort" prop="node_port">
                <el-input v-model="createService.node_port" placeholder="示例: 30001"></el-input>
              </el-form-item>
              <el-form-item class="SERVICE-create-form" label="标签" prop="label_str">
                <el-input v-model="createService.label_str" placeholder="示例: project=ms,app=gateway"></el-input>
              </el-form-item>
            </el-form>
          </el-col>
        </el-row>
      </template>
      <template #footer>
        <el-button @click="createServiceDrawer = false">取消</el-button>
        <el-button type="primary" @click="submitForm('createService')">立即创建</el-button>
      </template>
    </el-drawer>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
  data() {
    return {
      //编辑器配置
      cmOptions: common.cmOptions,
      contentYaml: '',
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //
      searchInput: '',
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      appLoading: false,
      serviceList: [],
      serviceTotal: 0,
      getServicesData: {
        url: common.k8sServiceList,
        params: {
          filter_name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //详情
      serviceDetail: {},
      getServiceDetailData: {
        url: common.k8sServiceDetail,
        params: {
          service_name: '',
          namespace: ''
        }
      },
      //yaml更新
      yamlDialog: false,
      updateServiceData: {
        url: common.k8sServiceUpdate,
        params: {
          namespace: '',
          content: ''
        }
      },
      //删除
      delServiceData: {
        url: common.k8sServiceDel,
        params: {
          service_name: '',
          namespace: '',
        }
      },
      //创建
      fullscreenLoading: false,
      direction: 'rtl',
      createServiceDrawer: false,
      createService: {
        name: '',
        namespace: '',
        type: 'ClusterIP',
        container_port: '',
        port: '',
        node_port: '',
        label: {},
        label_str: ''
      },
      createServiceData: {
        url: common.k8sServiceCreate,
        params: {}
      },
      createServiceRules: {
        name: [{
          required: true,
          message: '请填写名称',
          trigger: 'change'
        }],
        namespace: [{
          required: true,
          message: '请选择命名空间',
          trigger: 'change'
        }],
        port: [{
          required: true,
          message: '请填写Service端口',
          trigger: 'change'
        }],
        node_port: [{
          required: true,
          message: '请填写NodePort',
          trigger: 'change'
        }],
        label_str: [{
          required: true,
          message: '请填写标签',
          trigger: 'change'
        }],
        container_port: [{
          required: true,
          message: '请填写容器端口',
          trigger: 'change'
        }],
      },
    }
  },
  methods: {
    transYaml(content) {
      return json2yaml.stringify(content)
    },
    transObj(content) {
      return yaml2obj.load(content)
    },
    onChange(val) {
      this.contentYaml = val
    },
    handleSizeChange(size) {
      this.pagesize = size;
      this.getServices()
    },
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getServices()
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
    },
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    restartTotal(e) {
      let index, sum = 0
      let containerStatuses = e.row.status.containerStatuses
      for ( index in containerStatuses) {
        sum = sum + containerStatuses[index].restartCount
      }
      return sum
    },
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getServices() {
      this.appLoading = true
      this.getServicesData.params.filter_name = this.searchInput
      this.getServicesData.params.namespace = this.namespaceValue
      this.getServicesData.params.page = this.currentPage
      this.getServicesData.params.limit = this.pagesize
      httpClient.get(this.getServicesData.url, {params: this.getServicesData.params})
          .then(res => {
            this.serviceList = res.data.items
            this.serviceTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.appLoading = false
    },
    getServiceDetail(e) {
      this.getServiceDetailData.params.service_name = e.row.metadata.name
      this.getServiceDetailData.params.namespace = this.namespaceValue
      httpClient.get(this.getServiceDetailData.url, {params: this.getServiceDetailData.params})
          .then(res => {
            this.serviceDetail = res.data
            this.contentYaml = this.transYaml(this.serviceDetail)
            this.yamlDialog = true
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    updateService() {
      let content = JSON.stringify(this.transObj(this.contentYaml))
      this.updateServiceData.params.namespace = this.namespaceValue
      this.updateServiceData.params.content = content
      httpClient.put(this.updateServiceData.url, this.updateServiceData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.yamlDialog = false
    },
    delService(e) {
      this.delServiceData.params.service_name = e.row.metadata.name
      this.delServiceData.params.namespace = this.namespaceValue
      httpClient.delete(this.delServiceData.url, {data: this.delServiceData.params})
          .then(res => {
            this.getServices()
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
    createServiceFunc() {
      let reg = new RegExp("(^[A-Za-z]+=[A-Za-z0-9]+).*")
      if (!reg.test(this.createService.label_str)) {
        this.$message.warning({
          message: "标签填写异常,请确认后重新填写"
        })
        return
      }
      this.fullscreenLoading = true
      let label = new Map()
      let a = (this.createService.label_str).split(",")
      a.forEach(item => {
        let b = item.split("=")
        label[b[0]] = b[1]
      })
      this.createServiceData.params = this.createService
      this.createServiceData.params.label = label
      this.createServiceData.params.container_port = parseInt(this.createService.container_port)
      this.createServiceData.params.port = parseInt(this.createService.port)
      this.createServiceData.params.node_port = parseInt(this.createService.node_port)
      httpClient.post(this.createServiceData.url, this.createServiceData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
            this.getServices()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.resetForm('createService')
      this.fullscreenLoading = false
      this.createServiceDrawer = false
    },
    resetForm(formName) {
      this.$refs[formName].resetFields()
    },
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.createServiceFunc()
        } else {
          return false;
        }
      })
    }
  },
  watch: {
    namespaceValue: {
      handler() {
        localStorage.setItem('namespace', this.namespaceValue)
        this.currentPage = 1
        this.getServices()
      }
    },
  },
  beforeMount() {
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getServices()
  }
}
</script>


<style scoped>
.service-head-card,.service-body-card {
  border-radius: 1px;
  margin-bottom: 5px;
}
.service-head-search {
  width:160px;
  margin-right:10px;
}
.service-body-servicename {
  color: #4795EE;
}
.service-body-servicename:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
</style>

添加路由

src/router/index.js

    {
        path: '/loadbalance',
        name: "负载均衡",
        component: Layout,
        icon: "files", //图标
        meta: {title:"负载均衡", requireAuth: false},
        children: [
            {
                path: '/loadbalance/service',
                name: 'Service',
                icon: "el-icons-s-data", //图标
                meta: {title:"Service", requireAuth: true}, //定义meta元数据
                component: () => import('@/views/loadbalance/Service.vue')
            }
        ]
    },

测试 

4.2 Ingress

src/views/loadbalance/Ingress.vue

添加路由 

            {
                path: '/loadbalance/ingress',
                name: 'Ingress',
                icon: "el-icons-document-add", //图标
                meta: {title:"Ingress", requireAuth: true}, //定义meta元数据
                component: () => import('@/views/loadbalance/Ingress.vue')
            }

测试 

5、存储与配置

5.1 ConfigMap

src/views/storage/ConfigMap.vue

<template>
  <div class="configmap">
    <el-row>
      <el-col :span="24">
        <div>
          <el-card class="configmap-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="6">
                <div>
                  <span>命名空间: </span>
                  <el-select v-model="namespaceValue" filterable placeholder="请选择">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <el-col :span="2" :offset="16">
                <div>
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getConfigMaps()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="configmap-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="2">
                <div>
                  <el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
                </div>
              </el-col>
              <el-col :span="6">
                <div>
                  <el-input class="configmap-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getConfigMaps()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="configmap-body-card" shadow="never" :body-style="{padding:'5px'}">
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="configMapList"
                v-loading="appLoading">
              <el-table-column width="20"></el-table-column>
              <el-table-column align=left label="ConfigMap名">
                <template v-slot="scope">
                  <a class="configmap-body-configmapname">{
   
   { scope.row.metadata.name }}</a>
                </template>
              </el-table-column>
              <el-table-column align=center label="标签">
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.metadata.labels" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="key + ':' + val">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px" type="warning">{
   
   { ellipsis(key + ":" + val) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <el-table-column align=center label="DATA">
                <template v-slot="scope">
                  <el-popover
                      style="overflow:auto"
                      placement="right"
                      :width="400"
                      trigger="click">
                    <div style="overflow-y:auto;max-height:500px;">
                      <span>{
   
   { scope.row.data }}</span>
                    </div>
                    <template #reference>
                      <el-icon style="font-size:18px;cursor:pointer;"><reading/></el-icon>
                    </template>
                  </el-popover>
                </template>
              </el-table-column>
              <el-table-column align=center min-width="100" label="创建时间">
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="操作" width="200">
                <template v-slot="scope">
                  <el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getConfigMapDetail(scope)">YAML</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delConfigMap)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-pagination
                class="configmap-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                :page-size="pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="configMapTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
      <codemirror
          :value="contentYaml"
          border
          :options="cmOptions"
          height="500"
          style="font-size:14px;"
          @change="onChange"
      ></codemirror>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="yamlDialog = false">取 消</el-button>
                    <el-button type="primary" @click="updateConfigMap()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
  data() {
    return {
      //编辑器配置
      cmOptions: common.cmOptions,
      contentYaml: '',
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //
      searchInput: '',
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      appLoading: false,
      configMapList: [],
      configMapTotal: 0,
      getConfigMapsData: {
        url: common.k8sConfigMapList,
        params: {
          filter_name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //详情
      configMapDetail: {},
      getConfigMapDetailData: {
        url: common.k8sConfigMapDetail,
        params: {
          configmap_name: '',
          namespace: ''
        }
      },
      //yaml更新
      yamlDialog: false,
      updateConfigMapData: {
        url: common.k8sConfigMapUpdate,
        params: {
          namespace: '',
          content: ''
        }
      },
      //删除
      delConfigMapData: {
        url: common.k8sConfigMapDel,
        params: {
          configmap_name: '',
          namespace: '',
        }
      }
    }
  },
  methods: {
    transYaml(content) {
      return json2yaml.stringify(content)
    },
    transObj(content) {
      return yaml2obj.load(content)
    },
    onChange(val) {
      this.contentYaml = val
    },
    handleSizeChange(size) {
      this.pagesize = size;
      this.getConfigMaps()
    },
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getConfigMaps()
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
    },
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    restartTotal(e) {
      let index, sum = 0
      let containerStatuses = e.row.status.containerStatuses
      for ( index in containerStatuses) {
        sum = sum + containerStatuses[index].restartCount
      }
      return sum
    },
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getConfigMaps() {
      this.appLoading = true
      this.getConfigMapsData.params.filter_name = this.searchInput
      this.getConfigMapsData.params.namespace = this.namespaceValue
      this.getConfigMapsData.params.page = this.currentPage
      this.getConfigMapsData.params.limit = this.pagesize
      httpClient.get(this.getConfigMapsData.url, {params: this.getConfigMapsData.params})
          .then(res => {
            this.configMapList = res.data.items
            this.configMapTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.appLoading = false
    },
    getConfigMapDetail(e) {
      this.getConfigMapDetailData.params.configmap_name = e.row.metadata.name
      this.getConfigMapDetailData.params.namespace = this.namespaceValue
      httpClient.get(this.getConfigMapDetailData.url, {params: this.getConfigMapDetailData.params})
          .then(res => {
            this.configMapDetail = res.data
            this.contentYaml = this.transYaml(this.configMapDetail)
            this.yamlDialog = true
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    updateConfigMap() {
      let content = JSON.stringify(this.transObj(this.contentYaml))
      this.updateConfigMapData.params.namespace = this.namespaceValue
      this.updateConfigMapData.params.content = content
      httpClient.put(this.updateConfigMapData.url, this.updateConfigMapData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.yamlDialog = false
    },
    delConfigMap(e) {
      this.delConfigMapData.params.configmap_name = e.row.metadata.name
      this.delConfigMapData.params.namespace = this.namespaceValue
      httpClient.delete(this.delConfigMapData.url, {data: this.delConfigMapData.params})
          .then(res => {
            this.getConfigMaps()
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
  },
  watch: {
    namespaceValue: {
      handler() {
        localStorage.setItem('namespace', this.namespaceValue)
        this.currentPage = 1
        this.getConfigMaps()
      }
    },
  },
  beforeMount() {
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getConfigMaps()
  }
}
</script>


<style scoped>
.configmap-head-card,.configmap-body-card {
  border-radius: 1px;
  margin-bottom: 5px;
}
.configmap-head-search {
  width:160px;
  margin-right:10px;
}
.configmap-body-configmapname {
  color: #4795EE;
}
.configmap-body-configmapname:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
</style>

添加路由规则

 src/router/index.js

/storage路由下添加子路由:


    {
        path: '/storage',
        name: "存储",
        component: Layout,
        icon: "tickets", //图标
        meta: {title:"存储", requireAuth: false},
        children: [
            {
                path: '/storage/configmap',
                name: 'ConfigMap',
                icon: "el-icons-document-add", //图标
                meta: {title:"ConfigMap", requireAuth: true}, //定义meta元数据
                component: () => import('@/views/storage/ConfigMap.vue')
            }  
        ]
    }

创建cm用于测试删除cm kubectl apply -f configmap.yaml

 configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: game-demo
data:
  # 类属性键;每一个键都映射到一个简单的值
  player_initial_lives: "3"
  ui_properties_file_name: "user-interface.properties"

  # 类文件键
  game.properties: |
    enemy.types=aliens,monsters
    player.maximum-lives=5    
  user-interface.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true   

5.2 Secret

src/views/storage/Secret.vue

<template>
  <div class="secret">
    <el-row>
      <el-col :span="24">
        <div>
          <el-card class="secret-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="6">
                <div>
                  <span>命名空间: </span>
                  <el-select v-model="namespaceValue" filterable placeholder="请选择">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <el-col :span="2" :offset="16">
                <div>
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getSecrets()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="secret-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="2">
                <div>
                  <el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
                </div>
              </el-col>
              <el-col :span="6">
                <div>
                  <el-input class="secret-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getSecrets()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="secret-body-card" shadow="never" :body-style="{padding:'5px'}">
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="secretList"
                v-loading="appLoading">
              <el-table-column width="20"></el-table-column>
              <el-table-column align=left label="Secret名">
                <template v-slot="scope">
                  <a class="secret-body-secretname">{
   
   { scope.row.metadata.name }}</a>
                </template>
              </el-table-column>
              <el-table-column align=center label="标签">
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.metadata.labels" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="key + ':' + val">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px" type="warning">{
   
   { ellipsis(key + ":" + val) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <el-table-column align=center label="DATA">
                <template v-slot="scope">
                  <el-popover
                      style="overflow:auto"
                      placement="right"
                      :width="400"
                      trigger="click">
                    <div style="overflow-y:auto;max-height:500px;">
                      <span>{
   
   { scope.row.data }}</span>
                    </div>
                    <template #reference>
                      <el-icon style="font-size:18px;cursor:pointer;"><reading/></el-icon>
                    </template>
                  </el-popover>
                </template>
              </el-table-column>
              <el-table-column align=center prop="type" min-width="100" label="类型">
              </el-table-column>
              <el-table-column align=center min-width="100" label="创建时间">
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="操作" width="200">
                <template v-slot="scope">
                  <el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getSecretDetail(scope)">YAML</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delSecret)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-pagination
                class="secret-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                layout="total, sizes, prev, pager, next, jumper"
                :prev-click="getSecrets"
                :total="secretTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
      <codemirror
          :value="contentYaml"
          border
          :options="cmOptions"
          height="500"
          style="font-size:14px;"
          @change="onChange"
      ></codemirror>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="yamlDialog = false">取 消</el-button>
                    <el-button type="primary" @click="updateSecret()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
  data() {
    return {
      //编辑器配置
      cmOptions: common.cmOptions,
      contentYaml: '',
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //
      searchInput: '',
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      appLoading: false,
      secretList: [],
      secretTotal: 0,
      getSecretsData: {
        url: common.k8sSecretList,
        params: {
          filter_name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //详情
      secretDetail: {},
      getSecretDetailData: {
        url: common.k8sSecretDetail,
        params: {
          secret_name: '',
          namespace: ''
        }
      },
      //yaml更新
      yamlDialog: false,
      updateSecretData: {
        url: common.k8sSecretUpdate,
        params: {
          namespace: '',
          content: ''
        }
      },
      //删除
      delSecretData: {
        url: common.k8sSecretDel,
        params: {
          secret_name: '',
          namespace: '',
        }
      }
    }
  },
  methods: {
    transYaml(content) {
      return json2yaml.stringify(content)
    },
    transObj(content) {
      return yaml2obj.load(content)
    },
    onChange(val) {
      this.contentYaml = val
    },
    handleSizeChange(size) {
      this.pagesize = size;
      this.getSecrets()
    },
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getSecrets()
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
    },
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    restartTotal(e) {
      let index, sum = 0
      let containerStatuses = e.row.status.containerStatuses
      for ( index in containerStatuses) {
        sum = sum + containerStatuses[index].restartCount
      }
      return sum
    },
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getSecrets() {
      this.appLoading = true
      this.getSecretsData.params.filter_name = this.searchInput
      this.getSecretsData.params.namespace = this.namespaceValue
      this.getSecretsData.params.page = this.currentPage
      this.getSecretsData.params.limit = this.pagesize
      httpClient.get(this.getSecretsData.url, {params: this.getSecretsData.params})
          .then(res => {
            this.secretList = res.data.items
            this.secretTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.appLoading = false
    },
    getSecretDetail(e) {
      this.getSecretDetailData.params.secret_name = e.row.metadata.name
      this.getSecretDetailData.params.namespace = this.namespaceValue
      httpClient.get(this.getSecretDetailData.url, {params: this.getSecretDetailData.params})
          .then(res => {
            this.secretDetail = res.data
            this.contentYaml = this.transYaml(this.secretDetail)
            this.yamlDialog = true
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    updateSecret() {
      let content = JSON.stringify(this.transObj(this.contentYaml))
      this.updateSecretData.params.namespace = this.namespaceValue
      this.updateSecretData.params.content = content
      httpClient.put(this.updateSecretData.url, this.updateSecretData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.yamlDialog = false
    },
    delSecret(e) {
      this.delSecretData.params.secret_name = e.row.metadata.name
      this.delSecretData.params.namespace = this.namespaceValue
      httpClient.delete(this.delSecretData.url, {data: this.delSecretData.params})
          .then(res => {
            this.getSecrets()
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
  },
  watch: {
    namespaceValue: {
      handler() {
        localStorage.setItem('namespace', this.namespaceValue)
        this.currentPage = 1
        this.getSecrets()
      }
    },
  },
  beforeMount() {
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getSecrets()
  }
}
</script>


<style scoped>
.secret-head-card,.secret-body-card {
  border-radius: 1px;
  margin-bottom: 5px;
}
.secret-head-search {
  width:160px;
  margin-right:10px;
}
.secret-body-secretname {
  color: #4795EE;
}
.secret-body-secretname:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
</style>

添加路由src/router/index.js

            {
                path: '/storage/secret',
                name: 'Secret',
                icon: "el-icons-document-add", //图标
                meta: {title:"Secret", requireAuth: true}, //定义meta元数据
                component: () => import('@/views/storage/Secret.vue')
            }

创建secret.yaml用于测试删除secret 

5.3 Pvc

src/views/storage/Pvc.vue

<template>
  <div class="pvc">
    <el-row>
      <el-col :span="24">
        <div>
          <el-card class="pvc-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="6">
                <div>
                  <span>命名空间: </span>
                  <el-select v-model="namespaceValue" filterable placeholder="请选择">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <el-col :span="2" :offset="16">
                <div>
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getPvcs()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="pvc-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="2">
                <div>
                  <el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
                </div>
              </el-col>
              <el-col :span="6">
                <div>
                  <el-input class="pvc-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getPvcs()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <el-col :span="24">
        <div>
          <el-card class="pvc-body-card" shadow="never" :body-style="{padding:'5px'}">
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="pvcList"
                v-loading="appLoading">
              <el-table-column width="20"></el-table-column>
              <el-table-column align=left label="PVC名">
                <template v-slot="scope">
                  <a class="pvc-body-pvcname">{
   
   { scope.row.metadata.name }}</a>
                </template>
              </el-table-column>
              <el-table-column align=center label="标签">
                <template v-slot="scope">
                  <div v-for="(val, key) in scope.row.metadata.labels" :key="key">
                    <el-popover
                        placement="right"
                        :width="200"
                        trigger="hover"
                        :content="key + ':' + val">
                      <template #reference>
                        <el-tag style="margin-bottom: 5px" type="warning">{
   
   { ellipsis(key + ":" + val) }}</el-tag>
                      </template>
                    </el-popover>
                  </div>
                </template>
              </el-table-column>
              <el-table-column align=center label="状态">
                <template v-slot="scope">
                  <span :class="[scope.row.status.phase === 'Bound' ? 'success-status' : 'error-status']">{
   
   { scope.row.status.phase }}</span>
                </template>
              </el-table-column>
              <el-table-column align=center prop="status.capacity.storage" label="容量">
              </el-table-column>
              <el-table-column align=center prop="status.accessModes[0]" label="访问模式">
              </el-table-column>
              <el-table-column align=center prop="spec.storageClassName" label="StorageClass">
              </el-table-column>
              <el-table-column align=center min-width="100" label="创建时间">
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="操作" width="200">
                <template v-slot="scope">
                  <el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getPvcDetail(scope)">YAML</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delPvc)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-pagination
                class="pvc-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                :page-size="pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="pvcTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
      <codemirror
          :value="contentYaml"
          border
          :options="cmOptions"
          height="500"
          style="font-size:14px;"
          @change="onChange"
      ></codemirror>
      <template #footer>
                <span class="dialog-footer">
                    <el-button @click="yamlDialog = false">取 消</el-button>
                    <el-button type="primary" @click="updatePvc()">更 新</el-button>
                </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
  data() {
    return {
      //编辑器配置
      cmOptions: common.cmOptions,
      contentYaml: '',
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //
      searchInput: '',
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      appLoading: false,
      pvcList: [],
      pvcTotal: 0,
      getPvcsData: {
        url: common.k8sPvcList,
        params: {
          filter_name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //详情
      pvcDetail: {},
      getPvcDetailData: {
        url: common.k8sPvcDetail,
        params: {
          pvc_name: '',
          namespace: ''
        }
      },
      //yaml更新
      yamlDialog: false,
      updatePvcData: {
        url: common.k8sPvcUpdate,
        params: {
          namespace: '',
          content: ''
        }
      },
      //删除
      delPvcData: {
        url: common.k8sPvcDel,
        params: {
          pvc_name: '',
          namespace: '',
        }
      }
    }
  },
  methods: {
    transYaml(content) {
      return json2yaml.stringify(content)
    },
    transObj(content) {
      return yaml2obj.load(content)
    },
    onChange(val) {
      this.contentYaml = val
    },
    handleSizeChange(size) {
      this.pagesize = size;
      this.getPvcs()
    },
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getPvcs()
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
    },
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    restartTotal(e) {
      let index, sum = 0
      let containerStatuses = e.row.status.containerStatuses
      for ( index in containerStatuses) {
        sum = sum + containerStatuses[index].restartCount
      }
      return sum
    },
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getPvcs() {
      this.appLoading = true
      this.getPvcsData.params.filter_name = this.searchInput
      this.getPvcsData.params.namespace = this.namespaceValue
      this.getPvcsData.params.page = this.currentPage
      this.getPvcsData.params.limit = this.pagesize
      httpClient.get(this.getPvcsData.url, {params: this.getPvcsData.params})
          .then(res => {
            this.pvcList = res.data.items
            this.pvcTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.appLoading = false
    },
    getPvcDetail(e) {
      this.getPvcDetailData.params.pvc_name = e.row.metadata.name
      this.getPvcDetailData.params.namespace = this.namespaceValue
      httpClient.get(this.getPvcDetailData.url, {params: this.getPvcDetailData.params})
          .then(res => {
            this.pvcDetail = res.data
            this.contentYaml = this.transYaml(this.pvcDetail)
            this.yamlDialog = true
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    updatePvc() {
      let content = JSON.stringify(this.transObj(this.contentYaml))
      this.updatePvcData.params.namespace = this.namespaceValue
      this.updatePvcData.params.content = content
      httpClient.put(this.updatePvcData.url, this.updatePvcData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.yamlDialog = false
    },
    delPvc(e) {
      this.delPvcData.params.pvc_name = e.row.metadata.name
      this.delPvcData.params.namespace = this.namespaceValue
      httpClient.delete(this.delPvcData.url, {data: this.delPvcData.params})
          .then(res => {
            this.getPvcs()
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
  },
  watch: {
    namespaceValue: {
      handler() {
        localStorage.setItem('namespace', this.namespaceValue)
        this.currentPage = 1
        this.getPvcs()
      }
    },
  },
  beforeMount() {
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getPvcs()
  }
}
</script>


<style scoped>
.pvc-head-card,.pvc-body-card {
  border-radius: 1px;
  margin-bottom: 5px;
}
.pvc-head-search {
  width:160px;
  margin-right:10px;
}
.pvc-body-pvcname {
  color: #4795EE;
}
.pvc-body-pvcname:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
.success-status {
  color: rgb(27, 202, 21);
}
.warning-status {
  color: rgb(233, 200, 16);
}
.error-status {
  color: rgb(226, 23, 23);
}
</style>

 添加路由src/router/index.js

            {
                path: '/storage/pvc',
                name: 'Pvc',
                icon: "tickets", //图标
                meta: {title:"Pvc", requireAuth: true}, //定义meta元数据
                component: () => import('@/views/storage/Pvc.vue')
            }

创建pvc.yaml用于在平台上测试删除pvc 

Vue前端开发:仪表盘

6、概要

仪表盘src/views/home/Home.vue

<template>
  <div class="home">
    <!-- 折叠面板 -->
    <el-collapse v-model="activeNames">
      <!-- 面板1 集群资源卡片 -->
      <el-collapse-item title="集群资源" name="1">
        <el-row :gutter="10" style="margin-bottom: 10px;">
          <!-- 命名空间数量 -->
          <el-col :span="5">
            <el-card class="home-node-card" :body-style="{padding:'10px'}">
              <div style="float:left;padding-top:20%">
                <!-- 进度条 -->
                <!-- stroke-width 进度条的宽度 -->
                <!-- show-text 是否显示文字描述 -->
                <!-- percentage 进度百分比 -->
                <el-progress  :stroke-width="20" :show-text="false" type="circle" :percentage="namespaceActive/namespaceTotal * 100"></el-progress>
              </div>
              <div>
                <p class="home-node-card-title">命名空间: Active/总量</p>
                <p class="home-node-card-num">{
   
   { namespaceActive }}/{
   
   { namespaceTotal }}</p>
              </div>
            </el-card>
          </el-col>
          <!-- 服务数量 单个namespace中deployment的数量 -->
          <el-col :span="5">
            <el-card class="home-node-card" :body-style="{padding:'10px'}">
              <div>
                <p class="home-node-card-title">服务数</p>
                <p class="home-node-card-num">{
   
   { deploymentTotal }}</p>
              </div>
            </el-card>
          </el-col>
          <!-- 实例数 单个namespace中pod的数量 -->
          <el-col :span="5">
            <el-card class="home-node-card" :body-style="{padding:'10px'}">
              <div>
                <p class="home-node-card-title">实例数</p>
                <p class="home-node-card-num">{
   
   { podTotal }}</p>
              </div>
            </el-card>
          </el-col>
        </el-row>
      </el-collapse-item>
      <!-- 面板2 节点资源卡片 -->
      <el-collapse-item title="节点资源" name="2">
        <el-row :gutter="10" style="margin-bottom: 10px;">
          <!-- 节点数量 -->
          <el-col :span="5">
            <el-card class="home-node-card" :body-style="{padding:'10px'}">
              <div style="float:left;padding-top:20%">
                <el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="nodeTotal/nodeTotal * 100"></el-progress>
              </div>
              <div>
                <p class="home-node-card-title">节点: Ready/总数量</p>
                <p class="home-node-card-num">{
   
   { nodeTotal }}/{
   
   { nodeTotal }}</p>
              </div>
            </el-card>
          </el-col>
          <!-- CPU资源统计 -->
          <el-col :span="5">
            <el-card class="home-node-card" :body-style="{padding:'10px'}">
              <div style="float:left;padding-top:20%">
                <el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="nodeCpuAllocatable/nodeCpuCapacity * 100"></el-progress>
              </div>
              <div>
                <p class="home-node-card-title">CPU: 可分配/容量</p>
                <p class="home-node-card-num">{
   
   { nodeCpuAllocatable }}/{
   
   { nodeCpuCapacity }}</p>
              </div>
            </el-card>
          </el-col>
          <!-- 内存资源统计 -->
          <el-col :span="5">
            <el-card class="home-node-card" :body-style="{padding:'10px'}">
              <div style="float:left;padding-top:20%">
                <el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="nodeMemAllocatable/nodeMemCapacity * 100"></el-progress>
              </div>
              <div>
                <p class="home-node-card-title">内存: 可分配/容量</p>
                <p class="home-node-card-num">{
   
   { specTrans(nodeMemAllocatable) }}Gi/{
   
   { specTrans(nodeMemCapacity) }}Gi</p>
              </div>
            </el-card>
          </el-col>
          <!-- POD资源统计 -->
          <el-col :span="5">
            <el-card class="home-node-card" :body-style="{padding:'10px'}">
              <div style="float:left;padding-top:20%">
                <el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="nodePodAllocatable/nodePodAllocatable * 100"></el-progress>
              </div>
              <div>
                <p class="home-node-card-title">POD: 可分配/容量</p>
                <p class="home-node-card-num">{
   
   { nodePodAllocatable }}/{
   
   { nodePodAllocatable }}</p>
              </div>
            </el-card>
          </el-col>
        </el-row>
      </el-collapse-item>
      <!-- 面板3 资源统计画图 -->
      <el-collapse-item title="资源统计" name="3">
        <el-row :gutter="10">
          <!-- 每个namspace中pod数量的作图统计 -->
          <el-col :span="24" style="margin-bottom: 10px;">
            <el-card class="home-dash-card" :body-style="{padding:'10px'}">
              <!-- 这个div就是画图的内容,echarts初始化后会绑定到这个id上展示出来 -->
              <div id="podNumDash" style="height: 300px;">
              </div>
            </el-card>
          </el-col>
          <!-- 每个namespace中deployment数量的作图统计 -->
          <el-col :span="24">
            <el-card class="home-dash-card" :body-style="{padding:'10px'}">
              <div id="deployNumDash" style="height: 300px;">
              </div>
            </el-card>
          </el-col>
        </el-row>
      </el-collapse-item>
    </el-collapse>
  </div>
</template>

<script>
//引入echarts
import * as echarts from 'echarts'
import common from "../common/Config";
import httpClient from '../../utils/request';
export default {
  data() {
    return {
      //控制折叠面板的展开,表示打开所有的折叠面板
      activeNames: ["1", "2", "3"],
      //获取namespace的属性
      namespaceActive: 0,
      namespaceValue: 'default',
      namespaceTotal: 0,
      namespaceListUrl: common.k8sNamespaceList,
      //获取node的属性
      nodeTotal: 0,
      //cpu可分配
      nodeCpuAllocatable: 0,
      //cpu总量
      nodeCpuCapacity: 0,
      //内存可分配
      nodeMemAllocatable: 0,
      //内存总量
      nodeMemCapacity: 0,
      //pod可分配
      nodePodAllocatable: 0,
      //pod总量
      nodePodCapacity: 0,
      getNodesData: {
        url: common.k8sNodeList,
        params: {}
      },
      //获取deployment的数量
      deploymentTotal: 0,
      getDeploymentsData: {
        url: common.k8sDeploymentList,
        params: {
          namespace: '',
        }
      },
      //获取pod的数量
      podTotal: 0,
      getPodsData: {
        url: common.k8sPodList,
        params: {
          namespace: '',
        }
      },
      //每个namespace中pod的数量[{namespace:"default",pod_num:5}]
      podNumNp: [],
      podNumNpUrl: common.k8sPodNumNp,
      //每个namespace中deployment的数量[{namespace:"default",deployment_num:5}]
      deploymentNumNp: [],
      deploymentNumNpUrl: common.k8sDeploymentNumNp
    }
  },
  methods: {
    //获取namespace的数量
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceTotal = res.data.total
            let namespaceList = res.data.items
            //处理namespace active的数量
            let index
            for (index in namespaceList) {
              if (namespaceList[index].status.phase === "Active" ) {
                this.namespaceActive = this.namespaceActive + 1
              }
            }
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //用于内存数据的转换
    specTrans(num) {
      let a = num / 1024 / 1024
      //四舍五入保留小数点0位,也就是去除小数点
      return a.toFixed(0)
    },
    //获取node属性
    getNodes() {
      httpClient.get(this.getNodesData.url, {params: this.getNodesData.params})
          .then(res => {
            this.nodeTotal = res.data.total
            let nodeList = res.data.items
            let index
            for (index in nodeList) {
              //正则匹配纯数字,如果不是纯数字则跳过
              let isnum = /^\d+$/.test(nodeList[index].status.allocatable.cpu);
              if (!isnum) {
                continue
              }
              //计算node的cpu mem和pod的可分配及总容量数据
              this.nodeCpuAllocatable = parseInt(nodeList[index].status.allocatable.cpu) + this.nodeCpuAllocatable
              this.nodeCpuCapacity = parseInt(nodeList[index].status.capacity.cpu) + this.nodeCpuCapacity
              this.nodeMemAllocatable = parseInt(nodeList[index].status.allocatable.memory) + this.nodeMemAllocatable
              this.nodeMemCapacity = parseInt(nodeList[index].status.capacity.memory) + this.nodeMemCapacity
              this.nodePodAllocatable = parseInt(nodeList[index].status.allocatable.pods) + this.nodePodAllocatable
              this.nodePodCapacity = parseInt(nodeList[index].status.capacity.pods) + this.nodePodCapacity
            }
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //获取命名空间下的deployment总数
    getDeployments() {
      this.getDeploymentsData.params.namespace = this.namespaceValue
      httpClient.get(this.getDeploymentsData.url, {params: this.getDeploymentsData.params})
          .then(res => {
            this.deploymentTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //获取命名空间下的pod总数
    getPods() {
      this.getPodsData.params.namespace = this.namespaceValue
      httpClient.get(this.getPodsData.url, {params: this.getPodsData.params})
          .then(res => {
            this.podTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //获取每个namespace中deployment的数量
    getDeploymentNumNp() {
      httpClient.get(this.deploymentNumNpUrl)
          .then(res => {
            this.deploymentNumNp = res.data
            //echarts作图
            this.getDeployNumDash()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    //获取每个namespace中pod的数量
    getPodNumNp() {
      httpClient.get(this.podNumNpUrl)
          .then(res => {
            this.podNumNp = res.data
            //echarts作图
            this.getPodNumDash()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getPodNumDash(){
      //若实例已经初始化了,则销毁实例
      if (this.podNumDash != null && this.podNumDash != "" && this.podNumDash != undefined) {
        this.podNumDash.dispose()
      }
      //初始化实例,绑定到dom上
      this.podNumDash = echarts.init(document.getElementById('podNumDash'));
      //echarts作图配置
      this.podNumDash.setOption({
        //标题及字体颜色
        title: { text: 'Pods per Namespace', textStyle: {color:'rgb(134, 135, 136)'}},
        //图表颜色
        color: ['#67E033', '#9FE6B8', '#FFDB5C','#ff9f7f', '#fb7293', '#E062AE', '#E690D1', '#e7bcf3', '#9d96f5', '#8378EA', '#96BFFF'],
        //提示框
        tooltip: {
          //触发类型坐标轴触发
          trigger: "axis",
          //'cross' 十字准星指示器
          axisPointer: {
            type: "cross",
            label: {
              backgroundColor: "#76baf1"
            }
          }
        },
        //图表中的数据类型解释
        legend: {
          data: ['Pods']
        },
        //图表数据集
        dataset: {
          //维度定义,默认第一个元素表示x轴的数据,其他都是y轴数据
          dimensions: ['namespace','pod_num'],
          //源数据
          source: this.podNumNp
        },
        //x轴属性
        xAxis: {
          //category类目轴,value数值轴,time时间轴,log对数轴
          type: 'category',
          //轴标签
          axisLabel:{
            //坐标轴刻度标签的显示间隔,在类目轴中有效.0显示所有
            interval: 0,
            //格式化轴标签
            formatter: function (value) {
              return value.length>5?value.substring(0,5)+'...':value
            }
          },
        },
        //y轴属性
        yAxis: [
          //数值轴
          {type: 'value'}
        ],
        //定义系列,用于指定一组数值以及他们映射成的图
        series: [{
          //name是legend对应的值
          name: 'Pods',
          //bar柱状图,line折线图,pie饼图等等
          type: 'bar',
          //每个类目的值标签,配置
          label: {
            //是否显示值
            show: true,
            //显示的位置
            position: 'top'
          }
        }
        ]
      });
    },
    getDeployNumDash(){
      if (this.deployNumDash != null && this.deployNumDash != "" && this.deployNumDash != undefined) {
        this.deployNumDash.dispose()
      }
      this.deployNumDash = echarts.init(document.getElementById('deployNumDash'));

      this.deployNumDash.setOption({
        title: { text: 'Deployments per Namespace', textStyle: {color:'rgb(134, 135, 136)'}},
        color: ['#9FE6E8', '#FFDB5C','#ff9f7f', '#fb7293', '#E062AE', '#E690D1', '#e7bcf3', '#9d96f5', '#8378EA', '#96BFFF'],
        tooltip: { trigger: "axis", axisPointer: { type: "cross", label: { backgroundColor: "#76baf1" } } },
        legend: {
          data: ['Deployments']
        },
        dataset: {
          // 提供一份数据。
          dimensions: ['namespace','deployment_num'],
          source: this.deploymentNumNp
        },
        xAxis: {
          type: 'category',
          axisLabel:{
            interval: 0,
            formatter: function (value) {
              return value.length>5?value.substring(0,5)+'...':value
            }
          },
        },
        yAxis: [
          {type: 'value'}
        ],
        // 声明多个 bar 系列,默认情况下,每个系列会自动对应到 dataset 的每一列。
        series: [{
          name: 'Deployments',
          type: 'bar',
          label: {
            show: true,
            position: 'top'
          }
        }
        ]
      });
    },
  },
  beforeMount() {
    this.getNamespaces()
    this.getNodes()
    this.getDeployments()
    this.getPods()
    this.getDeploymentNumNp()
    this.getPodNumNp()
  }
}
</script>

<style scoped>
/deep/ .el-collapse-item__header {
  font-size: 16px;
}
.home-node-card {
  border-radius:1px;
  text-align: center;
  background-color: rgb(250, 253, 255);
}
.home-dash-card {
  border-radius:1px;
}
.home-node-card-title {
  font-size: 12px;
}
.home-node-card-num {
  font-size: 22px;
  font-weight: bold;
  color: rgb(63, 92, 135);
}
/deep/ .el-progress-circle {
  height: 50px !important;
  width: 50px !important;
}
</style>

 添加路由src/router/index.js

    {
        path: '/home', //视图
        component: Layout,
        icon: "odometer", //图标
        meta: {title:"Layout", requireAuth: false},
        children: [
            {
                path: '/home', //视图
                name: "集群状态",
                component: () => import('@/views/home/Home.vue'),
                icon: "odometer", //图标
                meta: {title:"集群状态", requireAuth: false}, //定义meta元数据
            },
        ]
    }

 Vue前端开发:工作流

7、工作流

(1)功能

(2)布局

(3)头部工具栏

(4)步骤条

抽屉弹出框1

 抽屉弹出框2

(5)数据表格

workflow信息

工作流是通过简单的表单就能完成deployment,service,ingress等一系列相关资源的创建。

src/views/workflow/Workflow.vue

<template>
  <div class="workflow">
    <el-row>
      <!-- header1 -->
      <el-col :span="24">
        <div>
          <el-card class="workflow-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="6">
                <div>
                  <span>命名空间: </span>
                  <el-select v-model="namespaceValue" filterable placeholder="请选择">
                    <el-option
                        v-for="(item, index) in namespaceList"
                        :key="index"
                        :label="item.metadata.name"
                        :value="item.metadata.name">
                    </el-option>
                  </el-select>
                </div>
              </el-col>
              <el-col :span="2" :offset="16">
                <div>
                  <el-button style="border-radius:2px;" icon="Refresh" plain @click="getWorkflows()">刷新</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <!-- header2 步骤条 -->
      <el-col :span="24">
        <div>
          <!-- 步骤条展示,active属性控制到了哪一步 -->
          <el-card class="workflow-head-card" shadow="never" :body-style="{padding:'30px 10px 20px 10px'}">
            <el-steps :active="active" align-center finish-status="success">
              <el-step title="步骤1" description="服务类型"></el-step>
              <el-step title="步骤2" description="填写表单"></el-step>
              <el-step title="步骤3" description="创建资源"></el-step>
            </el-steps>
          </el-card>
        </div>
      </el-col>
      <!-- header3 -->
      <el-col :span="24">
        <div>
          <el-card class="workflow-head-card" shadow="never" :body-style="{padding:'10px'}">
            <el-row>
              <el-col :span="3">
                <div>
                  <!-- 创建工作流 -->
                  <!-- createWorkflowDrawerIndex1-》createWorkflowDrawerIndex2-1-》createWorkflowDrawerIndex2-2 -->
                  <el-button style="border-radius:2px;" icon="Edit" type="primary" @click="createWorkflowDrawerIndex1 = true" v-loading.fullscreen.lock="fullscreenLoading">创建工作流</el-button>
                </div>
              </el-col>
              <el-col :span="6">
                <div>
                  <el-input class="workflow-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
                  <el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getWorkflows()">搜索</el-button>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </div>
      </el-col>
      <!-- 数据表格 -->
      <el-col :span="24">
        <div>
          <el-card class="workflow-body-card" shadow="never" :body-style="{padding:'5px'}">
            <el-table
                style="width:100%;font-size:12px;margin-bottom:10px;"
                :data="workflowList"
                v-loading="appLoading">
              <el-table-column width="20"></el-table-column>
              <el-table-column min-width="50" align=left label="ID" prop="id"></el-table-column>
              <el-table-column min-width="100" label="Workflow名">
                <template v-slot="scope">
                  <a class="workflow-body-workflowname">{
   
   { scope.row.name }}</a>
                </template>
              </el-table-column>
              <el-table-column label="类型" prop="type">
                <template v-slot="scope">
                  <el-tag type="warning">{
   
   { scope.row.type }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="实例数" prop="replicas"></el-table-column>
              <el-table-column min-width="100" label="deployment" prop="deployment"></el-table-column>
              <el-table-column min-width="150" label="service" prop="service"></el-table-column>
              <el-table-column min-width="150" label="ingress" prop="ingress"></el-table-column>
              <el-table-column align=center min-width="150" label="创建时间">
                <template v-slot="scope">
                  <el-tag type="info">{
   
   { timeTransNot8(scope.row.created_at) }} </el-tag>
                </template>
              </el-table-column>
              <el-table-column align=center label="操作" width="200">
                <template v-slot="scope">
                  <el-button size="small" disabled style="border-radius:2px;" icon="Edit" type="primary" plain @click="getWorkflowDetail(scope)">详情</el-button>
                  <el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delWorkflow)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-pagination
                class="workflow-body-pagination"
                background
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="currentPage"
                :page-sizes="pagesizeList"
                :page-size="pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="workflowTotal">
            </el-pagination>
          </el-card>
        </div>
      </el-col>
    </el-row>
    <!-- 抽屉弹框1 -->
    <el-drawer
        v-model="createWorkflowDrawerIndex1"
        :direction="direction"
        :before-close="handleClose">
      <template #title>
        <h4>创建Workflow-步骤1</h4>
      </template>
      <template #default>
        <el-row type="flex" justify="center">
          <el-col :span="20">
            <el-form label-width="80px">
              <el-form-item class="workflow-create-form" label="类型" prop="name">
                <el-radio v-model="createWorkflow.type" label="ClusterIP">ClusterIP</el-radio>
                <el-radio v-model="createWorkflow.type" label="NodePort">NodePort</el-radio>
                <el-radio v-model="createWorkflow.type" label="Ingress">Ingress</el-radio>
              </el-form-item>
            </el-form>
          </el-col>
        </el-row>
      </template>
      <template #footer>
        <el-button @click="drawerCancel('createWorkflowDrawerIndex1')">取消</el-button>
        <el-button type="primary" @click="workflowIndex1Next()">下一步</el-button>
      </template>
    </el-drawer>
    <!-- 抽屉弹框2 -->
    <el-drawer
        v-model="createWorkflowDrawerIndex2_1"
        :direction="direction"
        :before-close="handleClose">
      <template #title>
        <h4>创建Workflow-步骤2</h4>
      </template>
      <template #default>
        <el-row type="flex" justify="center">
          <el-col :span="20">
            <el-form ref="createWorkflow" :rules="createWorkflowRules" :model="createWorkflow" label-width="80px">
              <h4 style="margin-bottom:10px">Deployment</h4>
              <el-form-item class="workflow-create-form" label="名称" prop="name">
                <el-input v-model="createWorkflow.name"></el-input>
              </el-form-item>
              <el-form-item class="workflow-create-form" label="命名空间" prop="namespace">
                <el-select v-model="createWorkflow.namespace" filterable placeholder="请选择">
                  <el-option
                      v-for="(item, index) in namespaceList"
                      :key="index"
                      :label="item.metadata.name"
                      :value="item.metadata.name">
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item class="workflow-create-form" label="副本数" prop="replicas">
                <el-input-number v-model="createWorkflow.replicas" :min="1" :max="10"></el-input-number>
                <el-popover
                    placement="top"
                    :width="100"
                    trigger="hover"
                    content="申请副本数上限为10个">
                  <template #reference>
                    <el-icon style="width:2em;font-size:18px;color:#4795EE"><WarningFilled/></el-icon>
                  </template>
                </el-popover>
              </el-form-item>
              <el-form-item class="workflow-create-form" label="镜像" prop="image">
                <el-input v-model="createWorkflow.image"></el-input>
              </el-form-item>
              <el-form-item class="workflow-create-form" label="标签" prop="label_str">
                <el-input v-model="createWorkflow.label_str" placeholder="示例: project=ms,app=gateway"></el-input>
              </el-form-item>
              <el-form-item class="workflow-create-form" label="资源配额" prop="resource">
                <el-select v-model="createWorkflow.resource" placeholder="请选择">
                  <el-option value="0.5/1" label="0.5C1G"></el-option>
                  <el-option value="1/2" label="1C2G"></el-option>
                  <el-option value="2/4" label="2C4G"></el-option>
                  <el-option value="4/8" label="4C8G"></el-option>
                </el-select>
              </el-form-item>
              <el-form-item class="workflow-create-form" label="容器端口" prop="container_port">
                <el-input v-model="createWorkflow.container_port" placeholder="示例: 80"></el-input>
              </el-form-item>
              <el-form-item class="workflow-create-form" label="健康检查" prop="health">
                <el-switch v-model="createWorkflow.health_check" />
              </el-form-item>
              <el-form-item class="workflow-create-form" label="检查路径" prop="healthPath">
                <el-input v-model="createWorkflow.health_path" placeholder="示例: /health"></el-input>
              </el-form-item>
            </el-form>
          </el-col>
        </el-row>
      </template>
      <template #footer>
        <el-button @click="drawerCancel('createWorkflowDrawerIndex2_1')">取消</el-button>
        <el-button type="primary" @click="submitForm('createWorkflow', workflowIndex2_1Next)">下一步</el-button>
      </template>
    </el-drawer>
    <!-- 抽屉弹框3 -->
    <el-drawer
        v-model="createWorkflowDrawerIndex2_2"
        :direction="direction"
        :before-close="handleClose">
      <template #title>
        <h4>创建Workflow-步骤2</h4>
      </template>
      <template #default>
        <el-row type="flex" justify="center">
          <el-col :span="20">
            <el-form ref="createWorkflow" :rules="createWorkflowRules" :model="createWorkflow" label-width="80px">
              <h4 style="margin-bottom:10px">Service</h4>
              <el-form-item class="service-create-form" label="Service端口" prop="port">
                <el-input v-model="createWorkflow.port" placeholder="示例: 80"></el-input>
              </el-form-item>
              <el-form-item v-if="createWorkflow.type == 'NodePort'" class="service-create-form" label="NodePort" prop="node_port">
                <el-input v-model="createWorkflow.node_port" placeholder="示例: 30001"></el-input>
              </el-form-item>
              <el-divider v-if="createWorkflow.type == 'Ingress'"></el-divider>
              <h4 v-if="createWorkflow.type == 'Ingress'" style="margin-bottom:10px">Ingress</h4>
              <el-form-item v-if="createWorkflow.type == 'Ingress'" class="deploy-create-form" label="域名" prop="host">
                <el-input v-model="createWorkflow.host" placeholder="示例: www.example.com"></el-input>
              </el-form-item>
              <el-form-item v-if="createWorkflow.type == 'Ingress'" class="ingress-create-form" label="Path" prop="path">
                <el-input v-model="createWorkflow.path" placeholder="示例: /abc"></el-input>
              </el-form-item>
              <el-form-item v-if="createWorkflow.type == 'Ingress'" class="deploy-create-form" label="匹配类型" prop="path_type">
                <el-select v-model="createWorkflow.path_type" placeholder="请选择">
                  <el-option value="Prefix" label="Prefix"></el-option>
                  <el-option value="Exact" label="Exact"></el-option>
                  <el-option value="ImplementationSpecific" label="ImplementationSpecific"></el-option>
                </el-select>
              </el-form-item>
            </el-form>
          </el-col>
        </el-row>
      </template>
      <template #footer>
        <el-button @click="drawerCancel('createWorkflowDrawerIndex2_2')">取消</el-button>
        <el-button type="primary" @click="submitForm('createWorkflow', createWorkflowFunc)">立即创建</el-button>
      </template>
    </el-drawer>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
export default {
  data() {
    return {
      //工作流以及3个抽屉弹出框
      active: 0,
      createWorkflowDrawerIndex1: false,
      createWorkflowDrawerIndex2_1: false,
      createWorkflowDrawerIndex2_2: false,
      //分页
      currentPage: 1,
      pagesize: 10,
      pagesizeList: [10, 20, 30],
      //搜索及命名空间
      searchInput: '',
      namespaceValue: 'default',
      namespaceList: [],
      namespaceListUrl: common.k8sNamespaceList,
      //列表
      appLoading: false,
      workflowList: [],
      workflowTotal: 0,
      getWorkflowsData: {
        url: common.k8sWorkflowList,
        params: {
          name: '',
          namespace: '',
          page: '',
          limit: '',
        }
      },
      //创建
      fullscreenLoading: false,
      direction: 'rtl',
      createWorkflowDrawer: false,
      createWorkflow: {
        name: '',
        namespace: '',
        replicas: 1,
        image: '',
        resource: '',
        health_check: false,
        health_path: '',
        label_str: '',
        label: {},
        container_port: '',
        type: '',
        port: '',
        node_port: '',
        host: '',
        path: '',
        path_type: ''
      },
      createWorkflowData: {
        url: common.k8sWorkflowCreate,
        params: {}
      },
      createWorkflowRules: {
        name: [{
          required: true,
          message: '请填写名称',
          trigger: 'change'
        }],
        image: [{
          required: true,
          message: '请填写镜像',
          trigger: 'change'
        }],
        namespace: [{
          required: true,
          message: '请选择命名空间',
          trigger: 'change'
        }],
        resource: [{
          required: true,
          message: '请选择配额',
          trigger: 'change'
        }],
        label_str: [{
          required: true,
          message: '请填写标签',
          trigger: 'change'
        }],
        container_port: [{
          required: true,
          message: '请填写容器端口',
          trigger: 'change'
        }],
        type: [{
          required: true,
          message: '请填写工作流类型',
          trigger: 'change'
        }],
        port: [{
          required: true,
          message: '请填写Workflow端口',
          trigger: 'change'
        }],
        node_port: [{
          required: true,
          message: '请填写NodePort',
          trigger: 'change'
        }],
        host: [{
          required: true,
          message: '请填写域名',
          trigger: 'change'
        }],
        path: [{
          required: true,
          message: '请填写路径',
          trigger: 'change'
        }],
        path_type: [{
          required: true,
          message: '你选择匹配类型',
          trigger: 'change'
        }],
      },
      //删除
      delWorkflowData: {
        url: common.k8sWorkflowDel,
        params: {
          id: ''
        }
      },
    }
  },
  methods: {
    handleSizeChange(size) {
      this.pagesize = size;
      this.getWorkflows()
    },
    handleCurrentChange(currentPage) {
      this.currentPage = currentPage;
      this.getWorkflows()
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(() => {
            done();
          })
          .catch(() => {});
      this.active = 0
    },
    //关闭抽屉
    drawerCancel(drawerName) {
      switch (drawerName) {
        case 'createWorkflowDrawerIndex1':
          this.createWorkflowDrawerIndex1 = false
          break
        case 'createWorkflowDrawerIndex2_1':
          this.createWorkflowDrawerIndex2_1 = false
          break
        case 'createWorkflowDrawerIndex2_2':
          this.createWorkflowDrawerIndex2_2 = false
      }
      this.active = 0
    },
    ellipsis(value) {
      return value.length>15?value.substring(0,15)+'...':value
    },
    timeTrans(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    timeTransNot8(timestamp) {
      let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
      date = date.toJSON();
      date = date.substring(0, 19).replace('T', ' ')
      return date
    },
    getNamespaces() {
      httpClient.get(this.namespaceListUrl)
          .then(res => {
            this.namespaceList = res.data.items
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    },
    getWorkflows() {
      this.appLoading = true
      this.getWorkflowsData.params.name = this.searchInput
      this.getWorkflowsData.params.namespace = this.namespaceValue
      this.getWorkflowsData.params.page = this.currentPage
      this.getWorkflowsData.params.limit = this.pagesize
      httpClient.get(this.getWorkflowsData.url, {params: this.getWorkflowsData.params})
          .then(res => {
            this.workflowList = res.data.items
            this.workflowTotal = res.data.total
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.appLoading = false
    },
    delWorkflow(e) {
      this.delWorkflowData.params.id = e.row.id
      httpClient.delete(this.delWorkflowData.url, {data: this.delWorkflowData.params})
          .then(res => {
            this.getWorkflows()
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      console.log(123)
    },
    handleConfirm(obj, operateName, fn) {
      this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
      this.$confirm(this.confirmContent,'提示',{
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      })
          .then(() => {
            fn(obj)
          })
          .catch(() => {
            this.$message.info({
              message: '已取消操作'
            })
          })
    },
    //真正的创建workflow的方法
    createWorkflowFunc() {
      //验证标签,如果不符合a=b,c=d的格式,咱返回
      let reg = new RegExp("(^[A-Za-z]+=[A-Za-z0-9]+).*")
      if (!reg.test(this.createWorkflow.label_str)) {
        this.$message.warning({
          message: "标签填写异常,请确认后重新填写"
        })
        return
      }
      //加载动画开启
      this.fullscreenLoading = true
      //处理标签,将标签转成map   a=b  ->  map[a]=b
      let label = new Map()
      let cpu, memory
      let a = (this.createWorkflow.label_str).split(",")
      a.forEach(item => {
        let b = item.split("=")
        label[b[0]] = b[1]
      })
      //处理配额
      let resourceList = this.createWorkflow.resource.split("/")
      cpu = resourceList[0]
      memory = resourceList[1] + "Gi"
      //处理其他参数
      this.createWorkflowData.params = this.createWorkflow
      this.createWorkflowData.params.label = label
      this.createWorkflowData.params.cpu = cpu
      this.createWorkflowData.params.memory = memory
      this.createWorkflowData.params.container_port = parseInt(this.createWorkflow.container_port)
      this.createWorkflowData.params.port = parseInt(this.createWorkflow.port)
      this.createWorkflowData.params.node_port = parseInt(this.createWorkflow.node_port)
      //处理Hosts及httppath,跟后端处理相同,将数据转成map[host]=httpPaths的格式
      if (this.createWorkflow.type == 'Ingress') {
        let hosts = new Map()
        let httpPaths = []
        let httpPath = {
          path: this.createWorkflow.path,
          path_type: this.createWorkflow.path_type,
          service_name: this.createWorkflow.name,
          service_port: parseInt(this.createWorkflow.port)
        }
        httpPaths.push(httpPath)
        hosts[this.createWorkflow.host] = httpPaths
        this.createWorkflowData.params.hosts = hosts
      }
      //发送请求
      httpClient.post(this.createWorkflowData.url, this.createWorkflowData.params)
          .then(res => {
            this.$message.success({
              message: res.msg
            })
            this.getWorkflows()
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
      this.resetForm('createWorkflow')
      this.createWorkflowDrawerIndex2_2 = false
      this.active = 3
      this.fullscreenLoading = false
    },
    resetForm(formName) {
      this.$refs[formName].resetFields()
    },
    //抽屉2_2提交
    submitForm(formName, fn) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          fn()
        } else {
          return false;
        }
      })
    },
    //抽屉1的提交
    workflowIndex1Next() {
      //判断是否选择了type
      if (!this.createWorkflow.type) {
        this.$message.warning({
          message: "请选择工作流类型"
        })
        return
      }
      //关闭抽屉1
      this.createWorkflowDrawerIndex1 = false
      //打开抽屉2_1
      this.createWorkflowDrawerIndex2_1 = true
      //步骤条完成第一步
      this.active = 1
    },
    //抽屉2的提交
    workflowIndex2_1Next() {
      //关闭抽屉2_1
      this.createWorkflowDrawerIndex2_1 = false
      //打开抽屉2_2
      this.createWorkflowDrawerIndex2_2 = true
    }
  },
  watch: {
    namespaceValue: {
      handler() {
        localStorage.setItem('namespace', this.namespaceValue)
        this.currentPage = 1
        this.getWorkflows()
      }
    },
  },
  beforeMount() {
    if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
      this.namespaceValue = localStorage.getItem('namespace')
    }
    this.getNamespaces()
    this.getWorkflows()
  }
}
</script>


<style scoped>
.workflow-head-card,.workflow-body-card {
  border-radius: 1px;
  margin-bottom: 5px;
}
.workflow-head-search {
  width:160px;
  margin-right:10px;
}
.workflow-body-workflowname {
  color: #4795EE;
}
.workflow-body-workflowname:hover {
  color: rgb(84, 138, 238);
  cursor: pointer;
  font-weight: bold;
}
/deep/ .el-drawer__header {
  margin-bottom: 0px !important;
}
/deep/ .el-drawer__body {
  padding: 0px 0px 0px 0px;
}
</style>

添加路由

src/router/index.js

    {
        path: '/workflow',
        component: Layout,
        icon: "VideoPlay",
        children: [
            {
                path: "/workflow",
                name: "工作流",
                icon: "VideoPlay",
                meta: {title: "工作流", requireAuth: true},
                component: () => import('@/views/workflow/Workflow.vue')
            }
        ]
    }

 测试

Vue前端开发:登录登出、部署、总结

8、登录/登出

 (1)登录

账号密码验证

token生成

token验证

验证失败返回/login

验证成功进行跳转

(2)JWT校验

router/index.js

(3)登出 

删除Token

跳转/login

src/views/login/Login.vue

<template>
  <div class="login">
    <!-- 用户登录卡片 -->
    <el-card class="login-card">
      <template #header>
        <div class="login-card-header">
          <span>用户登录</span>
        </div>
      </template>
      <!-- 表单 -->
      <el-form :model="loginData" :rules="loginDataRules" ref="loginData">
        <el-form-item prop="username">
          <!-- 用户名 -->
          <el-input prefix-icon="UserFilled" v-model.trim="loginData.username" maxlength="32" placeholder="请输入账号" clearable></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <!-- 密码 -->
          <el-input prefix-icon="Lock" v-model.trim="loginData.password" maxlength="16" show-password placeholder="请输入密码" clearable></el-input>
        </el-form-item>
        <el-form-item>
          <!-- 登录按钮 -->
          <el-button type="primary" style="width: 100%;border-radius: 2px" :loading="loginLoading" @click="handleLogin">登 录</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import moment from 'moment';
import md5 from 'md5';

export default{
  data() {
    return {
      //加载等待动画
      loginLoading: false,
      //登录验证的后端接口
      loginUrl: common.loginAuth,
      loginData: {
        username: '',
        password: ''
      },
      //校验规则
      loginDataRules: {
        username: [{
          required: true,
          message: '请填写用户名',
          trigger: 'change'
        }],
        password: [{
          required: true,
          message: '请填写密码',
          trigger: 'change'
        }],
      }
    }
  },
  methods: {
    //登录方法
    handleLogin() {
      httpClient.post(this.loginUrl, this.loginData)
          .then(res => {
            //账号密码校验成功后的一系列操作
            localStorage.setItem('username', this.loginData.username);
            localStorage.setItem('loginDate', moment().format('YYYY-MM-DD_HH:mm:ss'));
            const salt = localStorage.getItem('username')+localStorage.getItem('loginDate')
            //生成token
            const tokenExpireTime = new Date(Date.now() + 24 * 60 * 60 * 1000); // 过期时间,24小时后
            // const token = jwt.sign(this.loginData.username, 'test', options);
            const token = md5(salt);
            localStorage.setItem('token', token); // 将Token保存到localStorage中
            localStorage.setItem('tokenExpireTime', tokenExpireTime.getTime().toString()); // 将过期时间保存到localStorage中
            //跳转至根路径
            this.$router.push('/');
            this.$message.success({
              message: res.msg
            })
          })
          .catch(res => {
            this.$message.error({
              message: res.msg
            })
          })
    }
  }
}
</script>

<style scoped>
.login {
  position: absolute;
  width: 100%;
  height: 100%;
  background: aquamarine;
  background-image: url(../../assets/img/login.png);
  background-size: 100%;
}
.login-card {
  position: absolute;
  left: 70%;
  top: 15%;
  width: 350px;
  border-radius: 5px;
  background: rgb(255, 255, 255);
  overflow: hidden;
}
.login-card-header {
  text-align: center;
}
</style>

 添加路由

src/router/index.js

    {
        path: '/login',  //url路径
        component: () => import('@/views/login/Login.vue'),  //视图组件
        meta: {title: "登录", requireAuth: false},  //meta元信息
    }

 src\router\index.js

验证是否有token和token是否过期.如果过期了就跳转到/login页面

// 导入md5
import md5 from 'md5';
//路由守卫,路由拦截
router.beforeEach((to, from, next) => {
    //启动进度条
    NProgress.start()
    //设置头部
    if (to.meta.title) {
        document.title = to.meta.title
    } else {
        document.title = "Kubernetes"
    }
    // 放行
    if (window.location.pathname == '/login') {
        next()
    }else{
        // 获取localStorage中保存的Token和过期时间
        const storedToken = localStorage.getItem('token');
        const storedTokenExpireTime = localStorage.getItem('tokenExpireTime');
        // 如果没有保存Token或过期时间,或者Token已经过期,则跳转到登录页面

        if (!storedToken || !storedTokenExpireTime || Date.now() > parseInt(storedTokenExpireTime)) {
            // 删除localStorage中保存的Token和过期时间
            localStorage.removeItem('token');
            localStorage.removeItem('tokenExpireTime');

            // 如果当前不在登录页面,则跳转到登录页面
            if (window.location.pathname !== '/login') {
                window.location.href = '/login';
            }
        } else {
            // 验证Token是否正确
            const salt = localStorage.getItem('username')+localStorage.getItem('loginDate')
            const token = md5(salt); // 使用md5算法生成Token

            if (token === storedToken) {
                // Token正确,且在有效期内,继续进行其他操作
                // TODO: 继续访问
                next()
            } else {
                // Token错误,跳转到登录页面
                localStorage.removeItem('token');
                localStorage.removeItem('tokenExpireTime');

                // 如果当前不在登录页面,则跳转到登录页面
                if (window.location.pathname !== '/login') {
                    window.location.href = '/login';
                }
            }
        }
    }
})

src/layout/Layout.vue

    //登出
    logout() {
      //移除用户名
      localStorage.removeItem('username');
      //移除token
      localStorage.removeItem('token');
      //跳转至/login页面
      this.$router.push('/login');
    }

 

 后端login方法待补充

六、部署前后端代码

1、前端

(1) 进入k8s-platform-fe项目根目录
(2) 删除/node_modules

(3) 执行 npm install

( 4) 运行 npm run serve
(5) 浏览器打开 localhost:7707
(6) 默认登录账号密码admin 123456

2、后端

(1) 要求golang版本1.13及以上
(2) 进入k8s-platform项目根目录
(3) 执行go mod tidy
( 4) 运行 go run main.go

(5)测试接口响应 curl --location --request GET --X GET 'http://0.0.0.0:9090/api/k8s/podsnamespace=kube-system'

Ps:由于启动了jwt验证,请求后端接口时需要携带Authorization头,故直接请求后端地址会报错。解决方式:打开main.go文件,注销第21行
r.Use(middle.JWTAuth())

七、总结

整个项目的前端页面开发完成,发现开发前端页面也就是固定的几个流程,布局->小视图->axios请求。

猜你喜欢

转载自blog.csdn.net/niwoxiangyu/article/details/130512090
今日推荐