前端
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个图片
- 固钉的k8s图片logo,src/assets/img/k8s-metris.png
- 登录用户的头像,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
功能:面包屑、下拉框、登出按钮
- 面包屑
- 展开关闭按钮
- 用户信息(退出按钮)
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请求。