浏览代码

添加 views/devops 模块

reghao 6 月之前
父节点
当前提交
936e31cfb5

+ 223 - 0
src/api/devops.js

@@ -0,0 +1,223 @@
+import { get, post, postForm } from '@/utils/request'
+
+const devopsApi = {
+  getDashboard: '/api/devops/dashboard',
+  getBuildDir: '/api/devops/build/dir',
+  eraseBuildDir: '/api/devops/build/dir/erase',
+  getMachineList: '/api/devops/machine/host',
+  getAliyunKeyList: '/api/devops/machine/aliyun/key',
+  getEnvList: '/api/devops/envs',
+  getAppTypeList: '/api/devops/app_types',
+  getCompilerList: '/api/devops/build/compiler',
+  getRepoAuthList: '/api/devops/build/repoauth',
+  getDockerRegistryList: '/api/devops/build/registry',
+  getPackerList: '/api/devops/build/packer',
+  getAppConfigList: '/api/devops/app/config/app',
+  getAppDeployConfigList: '/api/devops/app/config/app/deploy',
+  getBuildDeployList: '/api/devops/app/bd',
+  getAppStatList: '/api/devops/app/stat',
+  getMenuTree: '/api/devops/rbac/menu/ztree',
+  getRoleList: '/api/devops/rbac/role',
+  getUserList: '/api/devops/rbac/user',
+  getUserKeyList: '/bg/blog/post/list1'
+}
+
+export function getDashboard() {
+  return get(devopsApi.getDashboard)
+}
+
+export function getMachineList(queryInfo) {
+  return get(devopsApi.getMachineList, queryInfo)
+}
+
+export function getMachineUsedList(machineId) {
+  return get(devopsApi.getMachineList + '/app?machineId=' + machineId)
+}
+
+export function updateMachineEnv(payload) {
+  return postForm(devopsApi.getMachineList + '/env', payload)
+}
+
+export function deprecateMachine(payload) {
+  return postForm(devopsApi.getMachineList + '/deprecate', payload)
+}
+
+export function deleteMachine(payload) {
+  return postForm(devopsApi.getMachineList + '/delete', payload)
+}
+
+export function getAliyunKeyList() {
+  return get(devopsApi.getAliyunKeyList)
+}
+
+export function getEnvList() {
+  return get(devopsApi.getEnvList)
+}
+
+export function getAppTypeList() {
+  return get(devopsApi.getAppTypeList)
+}
+
+export function getBuildDir() {
+  return get(devopsApi.getBuildDir)
+}
+
+export function eraseBuildDir() {
+  return post(devopsApi.eraseBuildDir)
+}
+
+export function getRepoAuthList(pn) {
+  return get(devopsApi.getRepoAuthList + '?pn=' + pn)
+}
+
+export function getRepoTypes() {
+  return get(devopsApi.getRepoAuthList + '/repo_types')
+}
+
+export function addRepoAuth(formData) {
+  return postForm(devopsApi.getRepoAuthList, formData)
+}
+
+export function deleteRepoAuth(formData) {
+  return postForm(devopsApi.getRepoAuthList + '/delete', formData)
+}
+
+export function getCompilerList(pn) {
+  return get(devopsApi.getCompilerList + '?pn=' + pn)
+}
+
+export function getCompilerTypes() {
+  return get(devopsApi.getCompilerList + '/types')
+}
+
+export function getImageBindList(queryInfo) {
+  return get(devopsApi.getCompilerList + '/bind', queryInfo)
+}
+
+export function addCompiler(formData) {
+  return postForm(devopsApi.getCompilerList, formData)
+}
+
+export function deleteCompiler(formData) {
+  return postForm(devopsApi.getCompilerList + '/delete', formData)
+}
+
+export function getDockerRegistryList(pn) {
+  return get(devopsApi.getDockerRegistryList + '?pn=' + pn)
+}
+
+export function addDockerRegistry(formData) {
+  return postForm(devopsApi.getDockerRegistryList, formData)
+}
+
+export function deleteDockerRegistry(formData) {
+  return postForm(devopsApi.getDockerRegistryList + '/delete', formData)
+}
+
+export function getPackerList(pn) {
+  return get(devopsApi.getPackerList + '?pn=' + pn)
+}
+
+export function getPackTypes() {
+  return get(devopsApi.getPackerList + '/pack_types')
+}
+
+export function addPacker(formData) {
+  return postForm(devopsApi.getPackerList, formData)
+}
+
+export function deletePacker(formData) {
+  return postForm(devopsApi.getPackerList + '/delete', formData)
+}
+
+export function getAppConfigList(queryInfo) {
+  return get(devopsApi.getAppConfigList, queryInfo)
+}
+
+export function getBuildConfig() {
+  return get(devopsApi.getAppConfigList + '/build_config')
+}
+
+export function getAppConfig(appId) {
+  return get(devopsApi.getAppConfigList + '/detail?appId=' + appId)
+}
+
+export function getAppBindDomain(appId) {
+  return get(devopsApi.getAppConfigList + '/bind_domain?appId=' + appId)
+}
+
+export function addAppConfig(formData) {
+  return postForm(devopsApi.getAppConfigList, formData)
+}
+
+export function copyAppConfig(formData) {
+  return postForm(devopsApi.getAppConfigList + '/copy', formData)
+}
+
+export function updateAppConfig(formData) {
+  return postForm(devopsApi.getAppConfigList + '/update', formData)
+}
+
+export function eraseAppRepo(formData) {
+  return postForm(devopsApi.getAppConfigList + '/clear_repo', formData)
+}
+
+export function deleteAppConfig(formData) {
+  return postForm(devopsApi.getAppConfigList + '/delete', formData)
+}
+
+export function getAppDeployConfigList(appId) {
+  return get(devopsApi.getAppDeployConfigList + '?appId=' + appId)
+}
+
+export function getDeployMachineList(env) {
+  return get(devopsApi.getAppDeployConfigList + '/machine?env=' + env)
+}
+
+export function addAppDeployConfig(formData) {
+  return postForm(devopsApi.getAppDeployConfigList, formData)
+}
+
+export function updateAppDeployConfig(formData) {
+  return postForm(devopsApi.getAppDeployConfigList + '/update', formData)
+}
+
+export function deleteAppDeployConfig(formData) {
+  return postForm(devopsApi.getAppDeployConfigList + '/delete', formData)
+}
+
+export function getBuildDeployList(queryInfo) {
+  return get(devopsApi.getBuildDeployList + '/build', queryInfo)
+}
+
+export function getAppStatList(queryInfo) {
+  return get(devopsApi.getAppStatList, queryInfo)
+}
+
+export function getAppStat(appId) {
+  return get(devopsApi.getAppStatList + '/detail?appId=' + appId)
+}
+
+export function getMenuList() {
+  return get(devopsApi.getMenuTree)
+}
+
+export function getRoleList() {
+  return get(devopsApi.getRoleList)
+}
+
+export function getUserList() {
+  return get(devopsApi.getUserList)
+}
+
+export function updateUserNode(payload) {
+  return postForm(devopsApi.getUserKeyList, payload)
+}
+
+export function getBlogPosts() {
+  return get(devopsApi.getUserKeyList)
+}
+
+export function resetUserKey() {
+  return post(devopsApi.getUserKeyList)
+}

+ 243 - 0
src/router/devops.js

@@ -0,0 +1,243 @@
+// ********************************************************************************************************************
+const Background = () => import('views/devops/Background')
+const Dashboard = () => import('views/devops/Dashboard')
+
+// user
+const UserProfile = () => import('views/devops/user/UserProfile')
+const UserLogin = () => import('views/devops/user/UserLogin')
+const UserMessage = () => import('views/devops/user/UserMessage')
+// rbac
+const Menu = () => import('views/devops/rbac/Menu')
+const Role = () => import('views/devops/rbac/Role')
+const User = () => import('views/devops/rbac/User')
+// machine
+const MachineHost = () => import('views/devops/machine/MachineHost')
+const AliyunKey = () => import('views/devops/machine/AliyunKey')
+// build
+const BuildDir = () => import('views/devops/build/BuildDir')
+const RepoAuth = () => import('views/devops/build/RepoAuth')
+const Compiler = () => import('views/devops/build/Compiler')
+const DockerRegistry = () => import('views/devops/build/DockerRegistry')
+const Packer = () => import('views/devops/build/Packer')
+// app
+const AppConfig = () => import('views/devops/app/AppConfig')
+const BuildDeploy = () => import('views/devops/app/BuildDeploy')
+const AppStat = () => import('views/devops/app/AppStat')
+// file
+const FileList = () => import('views/devops/file/FileList')
+const ImageFile = () => import('views/devops/file/ImageFile')
+// sys
+const SiteConfig = () => import('views/devops/sys/SiteConfig')
+const AccessLog = () => import('views/devops/sys/AccessLog')
+const RuntimeLog = () => import('views/devops/sys/RuntimeLog')
+const RealtimeLog = () => import('views/devops/sys/RealtimeLog')
+const Webhook = () => import('views/devops/sys/Webhook')
+
+export default {
+  path: '/devops',
+  name: 'Admin',
+  component: Background,
+  meta: { needAuth: true },
+  children: [
+    {
+      path: '',
+      name: 'Dashboard',
+      component: Dashboard,
+      meta: { needAuth: true }
+    },
+    {
+      path: '/devops/user',
+      name: 'UserProfile',
+      component: { render: (e) => e('router-view') },
+      meta: { needAuth: true },
+      children: [
+        {
+          path: '/devops/user/profile',
+          name: 'UserProfile',
+          component: UserProfile,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/user/record',
+          name: 'UserLogin',
+          component: UserLogin,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/user/message',
+          name: 'UserMessage',
+          component: UserMessage,
+          meta: { needAuth: true }
+        }
+      ]
+    },
+    {
+      path: '/devops/machine',
+      name: 'MachineHost',
+      component: { render: (e) => e('router-view') },
+      meta: { needAuth: true },
+      children: [
+        {
+          path: '/devops/machine/host',
+          name: 'MachineHost',
+          component: MachineHost,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/machine/aliyun_key',
+          name: 'AliyunKey',
+          component: AliyunKey,
+          meta: { needAuth: true }
+        }
+      ]
+    },
+    {
+      path: '/devops/build',
+      name: 'BuildDir',
+      component: { render: (e) => e('router-view') },
+      meta: { needAuth: true },
+      children: [
+        {
+          path: '/devops/build/dir',
+          name: 'BuildDir',
+          component: BuildDir,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/build/repo_auth',
+          name: 'RepoAuth',
+          component: RepoAuth,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/build/compiler',
+          name: 'Compiler',
+          component: Compiler,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/build/docker_registry',
+          name: 'DockerRegistry',
+          component: DockerRegistry,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/build/packer',
+          name: 'Packer',
+          component: Packer,
+          meta: { needAuth: true }
+        }
+      ]
+    },
+    {
+      path: '/devops/app',
+      name: 'AppConfig',
+      component: { render: (e) => e('router-view') },
+      meta: { needAuth: true },
+      children: [
+        {
+          path: '/devops/app/config',
+          name: 'AppConfig',
+          component: AppConfig,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/app/bd',
+          name: 'BuildDeploy',
+          component: BuildDeploy,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/app/stat',
+          name: 'AppStat',
+          component: AppStat,
+          meta: { needAuth: true }
+        }
+      ]
+    },
+    {
+      path: '/devops/file',
+      name: 'FileList',
+      component: { render: (e) => e('router-view') },
+      meta: { needAuth: true },
+      children: [
+        {
+          path: '/devops/file/list',
+          name: 'FileList',
+          component: FileList,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/file/image',
+          name: 'ImageFile',
+          component: ImageFile,
+          meta: { needAuth: true }
+        }
+      ]
+    },
+    {
+      path: '/devops/sys',
+      name: 'SiteConfig',
+      component: { render: (e) => e('router-view') },
+      meta: { needAuth: true },
+      children: [
+        {
+          path: '/devops/sys/site',
+          name: 'SiteConfig',
+          component: SiteConfig,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/sys/access_log',
+          name: 'AccessLog',
+          component: AccessLog,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/sys/runtime_log',
+          name: 'RuntimeLog',
+          component: RuntimeLog,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/sys/realtime_log',
+          name: 'RealtimeLog',
+          component: RealtimeLog,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/sys/webhook',
+          name: 'Webhook',
+          component: Webhook,
+          meta: { needAuth: true }
+        }
+      ]
+    },
+    {
+      path: '/devops/rbac',
+      name: 'RBAC',
+      component: { render: (e) => e('router-view') },
+      meta: { needAuth: true },
+      children: [
+        {
+          path: '/devops/rbac/menu',
+          name: 'Menu',
+          component: Menu,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/rbac/role',
+          name: 'Role',
+          component: Role,
+          meta: { needAuth: true }
+        },
+        {
+          path: '/devops/rbac/user',
+          name: 'User',
+          component: User,
+          meta: { needAuth: true }
+        }
+      ]
+    }
+  ]
+}

+ 2 - 0
src/router/index.js

@@ -4,6 +4,7 @@ import Vue from 'vue'
 import DiskRouter from './disk'
 import UserRouter from './user'
 import SearchRouter from './search'
+import DevopsRouter from './devops'
 import BackgroundAccountRouter from './background_account'
 import BackgroundMyRouter from './background_my'
 import BackgroundPostRouter from './background_post'
@@ -41,6 +42,7 @@ export const constantRoutes = [
   DiskRouter,
   UserRouter,
   SearchRouter,
+  DevopsRouter,
   BackgroundAccountRouter,
   BackgroundMyRouter,
   BackgroundPostRouter,

+ 1 - 1
src/views/admin/AdminUserList.vue

@@ -184,7 +184,7 @@ export default {
         if (resp.code === 0) {
           const respData = resp.data
           this.dataList = respData.list
-          this.totalSize = respData.data.totalSize
+          this.totalSize = respData.totalSize
         } else {
           this.$message.error(resp.msg)
         }

+ 1 - 1
src/views/admin/AdminVideoList.vue

@@ -278,7 +278,7 @@ export default {
         if (resp.code === 0) {
           const respData = resp.data
           this.dataList = respData.list
-          this.totalSize = respData.data.totalSize
+          this.totalSize = respData.totalSize
         } else {
           this.$message.error(resp.msg)
         }

+ 75 - 0
src/views/devops/Background.vue

@@ -0,0 +1,75 @@
+<template>
+  <el-container class-name="main-container">
+    <el-aside :class="asideClass">
+      <LeftAside />
+    </el-aside>
+    <el-container>
+      <el-header class-name="main-header">
+        <TopNav />
+      </el-header>
+      <el-main class-name="main-center">
+        <router-view />
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script>
+import TopNav from '@/views/devops/TopNav.vue'
+import LeftAside from '@/views/devops/LeftAside.vue'
+
+export default {
+  name: 'Background',
+  components: {
+    TopNav,
+    LeftAside
+  },
+  data() {
+    return {
+      collapsed: false
+    }
+  },
+  computed: { // 计算属性
+    asideClass() { // 如果collapsed属性为true就展开不样式 反之就展开样式
+      return this.collapsed ? 'main-aside-collapsed' : 'main-aside'
+    }
+  },
+  created() { // 钩子函数
+    this.$root.Bus.$on('HandleSideMenu', value => {
+      this.collapsed = value
+    })
+  },
+  methods: {
+  }
+}
+</script>
+
+<style scoped>
+.main-container {
+  height: 100%;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+/* 不展开样式*/
+.main-aside-collapsed {
+  /* 在CSS中,通过对某一样式声明! important ,可以更改默认的CSS样式优先级规则,使该条样式属性声明具有最高优先级 */
+  width: 64px !important;
+  height: 100%;
+  background-color: #334157;
+  margin: 0px;
+}
+
+/* 展开样式*/
+.main-aside {
+  width: 240px !important;
+  height: 100%;
+  background-color: #334157;
+  margin: 0px;
+}
+
+.main-header, .main-center {
+  padding: 0px;
+  border-left: 2px solid #333;
+}
+</style>

+ 134 - 0
src/views/devops/Dashboard.vue

@@ -0,0 +1,134 @@
+<template>
+  <el-container>
+    <el-main class="movie-list">
+      <el-col :md="12" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
+        <el-card class="box-card">
+          <div slot="header" class="clearfix">
+            <span>机器节点</span>
+          </div>
+          <div class="text item">
+            <el-table
+              :data="machineStatList"
+              style="width: 100%"
+            >
+              <el-table-column
+                prop="env"
+                label="环境"
+              />
+              <el-table-column
+                prop="total"
+                label="总数"
+              />
+              <el-table-column
+                prop="onlineCount"
+                label="在线"
+              >
+                <template slot-scope="scope">
+                  <span style="color: green">{{ scope.row.onlineCount }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column
+                prop="offlineCount"
+                label="离线"
+              >
+                <template slot-scope="scope">
+                  <span style="color: red">{{ scope.row.offlineCount }}</span>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :md="12" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
+        <el-card class="box-card">
+          <div slot="header" class="clearfix">
+            <span>系统信息</span>
+          </div>
+          <div class="text item">
+            <el-descriptions class="margin-top" :column="1" :size="small" border>
+              <el-descriptions-item>
+                <template slot="label">
+                  <i class="el-icon-user" />
+                  应用版本
+                </template>
+                <a target="_blank" :href="`https://git.reghao.cn/reghao/bnt/commit/${sysInfo.commitId}`" style="text-decoration-line: none">
+                  {{ sysInfo.commitId }}
+                </a>
+              </el-descriptions-item>
+              <el-descriptions-item>
+                <template slot="label">
+                  <i class="el-icon-mobile-phone" />
+                  机器地址
+                </template>
+                {{ sysInfo.ipv4 }}
+              </el-descriptions-item>
+              <el-descriptions-item>
+                <template slot="label">
+                  <i class="el-icon-location-outline" />
+                  操作系统
+                </template>
+                {{ sysInfo.osInfo }}
+              </el-descriptions-item>
+              <el-descriptions-item>
+                <template slot="label">
+                  <i class="el-icon-tickets" />
+                  JVM
+                </template>
+                {{ sysInfo.jvmInfo }}
+              </el-descriptions-item>
+              <el-descriptions-item>
+                <template slot="label">
+                  <i class="el-icon-office-building" />
+                  启动时间
+                </template>
+                {{ sysInfo.startAt }}
+              </el-descriptions-item>
+              <el-descriptions-item>
+                <template slot="label">
+                  <i class="el-icon-office-building" />
+                  PID
+                </template>
+                {{ sysInfo.pid }}
+              </el-descriptions-item>
+            </el-descriptions>
+          </div>
+        </el-card>
+      </el-col>
+    </el-main>
+  </el-container>
+</template>
+
+<script>
+import { getDashboard } from '@/api/devops'
+
+export default {
+  name: 'Dashboard',
+  data() {
+    return {
+      machineStatList: [],
+      sysInfo: null
+    }
+  },
+  created() {
+    document.title = 'Dashboard'
+    this.getData()
+  },
+  methods: {
+    getData() {
+      getDashboard().then(resp => {
+        if (resp.code === 0) {
+          this.sysInfo = resp.data.sysInfo
+          this.machineStatList = resp.data.machineStatList
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 260 - 0
src/views/devops/LeftAside.vue

@@ -0,0 +1,260 @@
+<template>
+  <el-menu
+    :default-active="this.$route.path"
+    router
+    class="el-menu-vertical-demo"
+    background-color="#334157"
+    text-color="#fff"
+    active-text-color="#ffd04b"
+    :unique-opened="true"
+    :collapse="collapsed"
+    :collapse-transition="collapseTransition"
+  >
+    <div class="logobox">
+      <a href="/devops" style="text-decoration-line: none">
+        <img class="logoimg" src="@/assets/img/icon/logo.png" alt="">
+      </a>
+    </div>
+    <el-submenu v-for="(item, index) in menuList" :key="index" :index="item.url">
+      <template slot="title">
+        <i :class="item.icon" />
+        <span>{{ item.title }}</span>
+      </template>
+      <el-menu-item-group>
+        <el-menu-item v-for="(child, index0) in item.children" :key="index0" :index="child.url">
+          <i :class="child.icon" />
+          <span slot="title">{{ child.title }}</span>
+        </el-menu-item>
+      </el-menu-item-group>
+    </el-submenu>
+  </el-menu>
+</template>
+
+<script>
+export default {
+  name: 'LeftAside',
+  data() {
+    return {
+      collapsed: false,
+      collapseTransition: false,
+      menuList: []
+    }
+  },
+  mounted() {
+    this.initSideMenu()
+  },
+  created() {
+    // 钩子函数
+    this.$root.Bus.$on('HandleSideMenu', value => {
+      this.collapsed = value
+    })
+  },
+  methods: {
+    initSideMenu() {
+      this.menuList = [
+        {
+          url: '/devops/user',
+          title: '我的',
+          icon: 'el-icon-user',
+          children: [
+            {
+              url: '/devops/user/profile',
+              title: '我的资料',
+              icon: 'el-icon-user'
+            },
+            {
+              url: '/devops/user/record',
+              title: '登入记录',
+              icon: 'el-icon-user'
+            },
+            {
+              url: '/devops/user/message',
+              title: '我的消息',
+              icon: 'el-icon-user'
+            }
+          ]
+        },
+        {
+          url: '/devops/machine',
+          title: '机器',
+          icon: 'el-icon-s-data',
+          children: [
+            {
+              url: '/devops/machine/host',
+              title: '机器节点',
+              icon: 'el-icon-s-data'
+            },
+            {
+              url: '/devops/machine/aliyun_key',
+              title: '阿里云帐号',
+              icon: 'el-icon-s-data'
+            }
+          ]
+        },
+        {
+          url: '/devops/build',
+          title: '构建配置',
+          icon: 'el-icon-film',
+          children: [
+            {
+              url: '/devops/build/dir',
+              title: '构建目录',
+              icon: 'el-icon-film'
+            },
+            {
+              url: '/devops/build/repo_auth',
+              title: '仓库认证',
+              icon: 'el-icon-film'
+            },
+            {
+              url: '/devops/build/compiler',
+              title: '编译器',
+              icon: 'el-icon-film'
+            },
+            {
+              url: '/devops/build/docker_registry',
+              title: 'docker 仓库',
+              icon: 'el-icon-files'
+            },
+            {
+              url: '/devops/build/packer',
+              title: '应用打包',
+              icon: 'el-icon-files'
+            }
+          ]
+        },
+        {
+          url: '/devops/app',
+          title: '应用',
+          icon: 'el-icon-files',
+          children: [
+            {
+              url: '/devops/app/config',
+              title: '应用配置',
+              icon: 'el-icon-files'
+            },
+            {
+              url: '/devops/app/bd',
+              title: '构建部署',
+              icon: 'el-icon-files'
+            },
+            {
+              url: '/devops/app/stat',
+              title: '运行状态',
+              icon: 'el-icon-files'
+            }
+          ]
+        },
+        {
+          url: '/devops/file',
+          title: '文件',
+          icon: 'el-icon-setting',
+          children: [
+            {
+              url: '/devops/file/list',
+              title: '我的文件',
+              icon: 'el-icon-setting'
+            },
+            {
+              url: '/devops/file/image',
+              title: '我的图片',
+              icon: 'el-icon-setting'
+            }
+          ]
+        },
+        {
+          url: '/devops/sys',
+          title: '系统',
+          icon: 'el-icon-user-solid',
+          children: [
+            {
+              url: '/devops/sys/site',
+              title: '站点配置',
+              icon: 'el-icon-user-solid'
+            },
+            {
+              url: '/devops/sys/access_log',
+              title: '访问日志',
+              icon: 'el-icon-user-solid'
+            },
+            {
+              url: '/devops/sys/runtime_log',
+              title: '运行日志',
+              icon: 'el-icon-user-solid'
+            },
+            {
+              url: '/devops/sys/realtime_log',
+              title: '实时日志',
+              icon: 'el-icon-user-solid'
+            },
+            {
+              url: '/devops/sys/webhook',
+              title: 'webhook通知',
+              icon: 'el-icon-user-solid'
+            }
+          ]
+        },
+        {
+          url: '/devops/rbac',
+          title: 'RBAC',
+          icon: 'el-icon-loading',
+          children: [
+            {
+              url: '/devops/rbac/menu',
+              title: '资源管理',
+              icon: 'el-icon-loading'
+            },
+            {
+              url: '/devops/rbac/role',
+              title: '角色管理',
+              icon: 'el-icon-loading'
+            },
+            {
+              url: '/devops/rbac/user',
+              title: '用户管理',
+              icon: 'el-icon-loading'
+            }
+          ]
+        }
+      ]
+    }
+  }
+}
+</script>
+
+<style>
+.el-menu-vertical-demo:not(.el-menu--collapse) {
+  width: 180px;
+  min-height: 720px;
+}
+
+.el-menu-vertical-demo:not(.el-menu--collapse) {
+  border: none;
+  text-align: left;
+}
+
+.el-menu-item-group__title {
+  padding: 0px;
+}
+
+.el-menu-bg {
+  background-color: #1f2d3d !important;
+}
+
+.el-menu {
+  border: none;
+}
+
+.logobox {
+  height: 40px;
+  line-height: 40px;
+  color: #9d9d9d;
+  font-size: 20px;
+  text-align: center;
+  padding: 20px 0px;
+}
+
+.logoimg {
+  height: 40px;
+}
+</style>

+ 78 - 0
src/views/devops/TopNav.vue

@@ -0,0 +1,78 @@
+<template>
+  <el-menu
+    class="el-menu-demo"
+    mode="horizontal"
+    background-color="#334157"
+    text-color="#fff"
+    active-text-color="#fff"
+  >
+    <el-button
+      class="button_icon"
+      :icon="collapsed ? 'el-icon-s-fold' : 'el-icon-s-unfold'"
+      @click="doToggle()"
+    />
+    <el-submenu index="2" class="submenu">
+      <template slot="title">{{ user.screenName }}</template>
+      <el-menu-item index="2-1" @click="backToHome">
+        <i class="el-icon-s-home" />
+        <span slot="title">回到主站</span>
+      </el-menu-item>
+      <el-menu-item index="2-3" @click.native="goToLogout">
+        <i class="el-icon-close" />
+        <span slot="title">登出</span>
+      </el-menu-item>
+    </el-submenu>
+  </el-menu>
+</template>
+
+<script>
+import { userMixin } from 'assets/js/mixin'
+import { getAuthedUser } from '@/utils/auth'
+
+export default {
+  name: 'TopNav',
+  mixins: [userMixin],
+  data() {
+    return {
+      user: null,
+      collapsed: false,
+      imgshow: require('@/assets/img/logo.png'),
+      imgsq: require('@/assets/img/logo.png')
+    }
+  },
+  created() {
+    this.user = getAuthedUser()
+  },
+  methods: {
+    doToggle() {
+      // 主要控制 collapsed 为 true 或 false
+      this.collapsed = !this.collapsed
+      this.$root.Bus.$emit('HandleSideMenu', this.collapsed)
+    },
+    backToHome() {
+      const path = '/'
+      if (this.$route.path === path) {
+        this.$router.go(0)
+        return
+      }
+      this.$router.push(path)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.el-menu-vertical-demo:not(.el-menu--collapse) {
+  border: none;
+}
+
+.submenu {
+  float: right;
+}
+
+.button_icon {
+  height: 60px;
+  background-color: transparent;
+  border: none;
+}
+</style>

+ 800 - 0
src/views/devops/app/AppConfig.vue

@@ -0,0 +1,800 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>应用配置列表</h3>
+      <el-row style="margin-top: 10px">
+        <el-select
+          v-model="queryInfo.env"
+          clearable
+          placeholder="环境"
+          style="margin-left: 5px"
+          @change="onSelectChange"
+        >
+          <el-option
+            v-for="(item, index) in envList"
+            :key="index"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+        <el-select
+          v-model="queryInfo.appType"
+          clearable
+          placeholder="类型"
+          style="margin-left: 5px"
+          @change="onSelectChange"
+        >
+          <el-option
+            v-for="(item, index) in appTypeList"
+            :key="index"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+        <el-button type="success" icon="el-icon-plus" style="margin-left: 5px" @click="handleAdd">添加</el-button>
+      </el-row>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="appName"
+          label="应用名"
+        />
+        <el-table-column
+          prop="appId"
+          label="应用 ID"
+        />
+        <el-table-column
+          prop="repoBranch"
+          label="分支"
+        />
+        <el-table-column
+          prop="bindPorts"
+          label="监听端口"
+        />
+        <el-table-column
+          prop="totalDeployNodes"
+          label="部署配置"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tag disable-transitions>
+              <span>{{ scope.row.totalDeployNodes }}</span>
+            </el-tag>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              type="success"
+              @click="handleDeployConfig(scope.$index, scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="totalDomains"
+          label="关联域名"
+        >
+          <template slot-scope="scope">
+            <el-tag disable-transitions>
+              <span>{{ scope.row.totalDomains }}</span>
+            </el-tag>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              type="success"
+              @click="handleBindDomain(scope.$index, scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              @click="handleCopy(scope.$index, scope.row)"
+            >拷贝</el-button>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >详情</el-button>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >编辑</el-button>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              type="danger"
+              @click="handleDelete(scope.$index, scope.row)"
+            >删除</el-button>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              @click="handleErase(scope.$index, scope.row)"
+            >清空本地仓库</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <el-dialog
+      title="关联域名列表"
+      append-to-body
+      :visible.sync="showBindDomainDialog"
+      center
+    >
+      <template>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="拷贝应用配置"
+      append-to-body
+      :visible.sync="showCopyDialog"
+      center
+    >
+      <template>
+        <el-form :model="copyForm" label-width="80px">
+          <el-form-item label="新应用 ID">
+            <el-input v-model="copyForm.newAppId" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="新应用环境">
+            <el-select v-model="copyForm.newEnv" placeholder="选择环境">
+              <el-option
+                v-for="(item, index) in envList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="新应用分支" style="width: 70%; padding-right: 2px">
+            <el-input v-model="copyForm.newRepoBranch" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onCopy">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="编辑应用配置"
+      append-to-body
+      :visible.sync="showEditDialog"
+      center
+    >
+      <template>
+        <el-form :model="editForm" label-width="80px">
+          <el-form-item label="应用 ID" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editForm.appId" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="应用名" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editForm.appName" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="应用仓库" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editForm.appRepo" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="仓库分支" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editForm.repoBranch" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="应用路径" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editForm.appRootPath" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="监听端口" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editForm.bindPorts" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="仓库认证">
+            <el-select v-model="editForm.repoAuthConfig" placeholder="选择仓库认证">
+              <el-option
+                v-for="(item, index) in repoAuthList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="编译工具">
+            <el-select v-model="editForm.compilerConfig" placeholder="选择编译工具">
+              <el-option
+                v-for="(item, index) in compilerList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="打包工具">
+            <el-select v-model="editForm.packerConfig" placeholder="选择打包工具">
+              <el-option
+                v-for="(item, index) in packerList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="Dockerfile" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editForm.dockerfile" type="textarea" :rows="10" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onEdit">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="添加应用配置"
+      append-to-body
+      :visible.sync="showAddDialog"
+      center
+    >
+      <template>
+        <el-form :model="addForm" label-width="80px">
+          <el-form-item label="应用类型">
+            <el-select v-model="addForm.appType" placeholder="选择类型">
+              <el-option
+                v-for="(item, index) in appTypeList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="所属环境">
+            <el-select v-model="addForm.env" placeholder="选择环境">
+              <el-option
+                v-for="(item, index) in envList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="应用 ID" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addForm.appId" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="应用名" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addForm.appName" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="应用仓库" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addForm.appRepo" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="仓库分支" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addForm.repoBranch" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="应用路径" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addForm.appRootPath" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="监听端口" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addForm.bindPorts" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="仓库认证">
+            <el-select v-model="addForm.repoAuthConfig" placeholder="选择仓库认证">
+              <el-option
+                v-for="(item, index) in repoAuthList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="编译工具">
+            <el-select v-model="addForm.compilerConfig" placeholder="选择编译工具">
+              <el-option
+                v-for="(item, index) in compilerList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="打包工具">
+            <el-select v-model="addForm.packerConfig" placeholder="选择打包工具">
+              <el-option
+                v-for="(item, index) in packerList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="Dockerfile" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addForm.dockerfile" type="textarea" :rows="10" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onAdd">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="部署配置列表"
+      append-to-body
+      :visible.sync="showDeployConfigDialog"
+      width="70%"
+      center
+    >
+      <template>
+        <el-button type="success" icon="el-icon-plus" style="margin-bottom: 5px" @click="handleAddDeployConfig">添加</el-button>
+        <el-table
+          :data="appDeployConfigList"
+          border
+          height="480"
+          style="width: 100%"
+        >
+          <el-table-column
+            prop="appName"
+            label="应用名"
+          />
+          <el-table-column
+            prop="machineIpv4"
+            label="机器地址"
+          />
+          <el-table-column
+            prop="packType"
+            label="打包类型"
+          />
+          <el-table-column
+            prop="startScript"
+            label="启动脚本"
+          />
+          <el-table-column
+            fixed="right"
+            label="操作"
+            width="280"
+          >
+            <template slot-scope="scope">
+              <el-button
+                style="margin-top: 5px; margin-left: 5px"
+                size="mini"
+                @click="handleEditDeployConfig(scope.$index, scope.row)"
+              >编辑</el-button>
+              <el-button
+                style="margin-top: 5px; margin-left: 5px"
+                size="mini"
+                type="danger"
+                @click="handleDeleteDeployConfig(scope.$index, scope.row)"
+              >删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="添加部署配置"
+      append-to-body
+      :visible.sync="showAddDeployConfigDialog"
+      center
+    >
+      <template>
+        <el-form :model="addDeployForm" label-width="80px">
+          <el-form-item label="应用 ID" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addDeployForm.appId" style="width: 70%; padding-right: 2px" readonly />
+          </el-form-item>
+          <el-form-item label="打包类型" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addDeployForm.packType" style="width: 70%; padding-right: 2px" readonly />
+          </el-form-item>
+          <el-form-item label="选择机器">
+            <el-select v-model="addDeployForm.machineId" placeholder="选择机器">
+              <el-option
+                v-for="(item, index) in machineList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="启动脚本" style="width: 70%; padding-right: 2px">
+            <el-input v-model="addDeployForm.startScript" type="textarea" :rows="10" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onAddDeploy">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="编辑部署配置"
+      append-to-body
+      :visible.sync="showEditDeployConfigDialog"
+      center
+    >
+      <template>
+        <el-form :model="editDeployForm" label-width="80px">
+          <el-form-item label="应用 ID" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editDeployForm.appId" style="width: 70%; padding-right: 2px" readonly />
+          </el-form-item>
+          <el-form-item label="打包类型" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editDeployForm.packType" style="width: 70%; padding-right: 2px" readonly />
+          </el-form-item>
+          <el-form-item label="选择机器">
+            <el-select v-model="editDeployForm.machineId" placeholder="选择机器">
+              <el-option
+                v-for="(item, index) in machineList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="启动脚本" style="width: 70%; padding-right: 2px">
+            <el-input v-model="editDeployForm.startScript" type="textarea" :rows="10" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onEditDeploy">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import {
+  addAppConfig, addAppDeployConfig,
+  copyAppConfig,
+  deleteAppConfig, deleteAppDeployConfig,
+  eraseAppRepo,
+  getAppConfig,
+  getAppConfigList,
+  getAppDeployConfigList,
+  getAppTypeList,
+  getBuildConfig,
+  getDeployMachineList,
+  getEnvList,
+  updateAppConfig, updateAppDeployConfig
+} from '@/api/devops'
+
+export default {
+  name: 'AppConfig',
+  data() {
+    return {
+      envList: [],
+      appTypeList: [],
+      queryInfo: {
+        env: 'test',
+        appType: 'java',
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      // **********************************************************************
+      showBindDomainDialog: false,
+      // **********************************************************************
+      showAddDialog: false,
+      repoAuthList: [],
+      compilerList: [],
+      packerList: [],
+      addForm: {
+        appType: null,
+        env: null,
+        appId: null,
+        appName: null,
+        appRepo: null,
+        repoBranch: null,
+        appRootPath: null,
+        bindPorts: null,
+        repoAuthConfig: null,
+        compilerConfig: null,
+        packerConfig: null,
+        dockerfile: null
+      },
+      // **********************************************************************
+      showCopyDialog: false,
+      copyForm: {
+        appId: null,
+        newAppId: null,
+        newEnv: null,
+        newRepoBranch: null
+      },
+      // **********************************************************************
+      showEditDialog: false,
+      editForm: {
+        appId: null
+      },
+      // **********************************************************************
+      showDeployConfigDialog: false,
+      appDeployConfigList: [],
+      showAddDeployConfigDialog: false,
+      machineList: [],
+      packTypes: [],
+      addDeployForm: {
+        appId: null,
+        packType: null,
+        machineId: null,
+        machineIpv4: null,
+        startScript: null
+      },
+      showEditDeployConfigDialog: false,
+      editDeployForm: {
+        appId: null,
+        packType: null,
+        machineId: null,
+        machineIpv4: null,
+        startScript: null
+      }
+    }
+  },
+  created() {
+    const env = this.$route.query.env
+    if (env !== undefined && env !== null) {
+      this.queryInfo.env = env
+    }
+    const appType = this.$route.query.appType
+    if (appType !== undefined && appType !== null) {
+      this.queryInfo.appType = appType
+    }
+    const pageNumber = this.$route.query.pn
+    if (pageNumber !== undefined && pageNumber !== null) {
+      this.currentPage = parseInt(pageNumber)
+      this.queryInfo.pn = parseInt(pageNumber)
+    }
+
+    document.title = '应用配置列表'
+    getEnvList().then(resp => {
+      if (resp.code === 0) {
+        this.envList = resp.data
+      } else {
+        this.$message.error(resp.msg)
+      }
+    }).catch(error => {
+      this.$message.error(error.message)
+    })
+    getAppTypeList().then(resp => {
+      if (resp.code === 0) {
+        this.appTypeList = resp.data
+      } else {
+        this.$message.error(resp.msg)
+      }
+    }).catch(error => {
+      this.$message.error(error.message)
+    })
+
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.queryInfo.pn = pageNumber
+      this.$router.push({
+        path: '/devops/app/config',
+        query: this.queryInfo
+      })
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getAppConfigList(this.queryInfo).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleBindDomain(index, row) {
+      this.showBindDomainDialog = true
+    },
+    handleCopy(index, row) {
+      this.copyForm.appId = row.appId
+      this.showCopyDialog = true
+    },
+    onCopy() {
+      const formData = new FormData()
+      formData.append('appId', this.copyForm.appId)
+      formData.append('newAppId', this.copyForm.newAppId)
+      formData.append('newEnv', this.copyForm.newEnv)
+      formData.append('newRepoBranch', this.copyForm.newRepoBranch)
+      copyAppConfig(formData).then(resp => {
+        this.$message.info(resp.msg)
+        this.getData()
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showEditDialog = false
+      })
+    },
+    handleEdit(index, row) {
+      getAppConfig(row.appId).then(resp => {
+        if (resp.code === 0) {
+          this.editForm = resp.data
+          this.showEditDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    onEdit() {
+      const formData = new FormData()
+      formData.append('appId', this.editForm.appId)
+      updateAppConfig(formData).then(resp => {
+        if (resp.code === 0) {
+          this.getData()
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showEditDialog = false
+      })
+    },
+    handleAdd() {
+      getBuildConfig().then(resp => {
+        if (resp.code === 0) {
+          this.repoAuthList = resp.data.repoAuthList
+          this.compilerList = resp.data.compilerList
+          this.packerList = resp.data.packerList
+          this.showAddDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    onAdd() {
+      const formData = new FormData()
+      formData.append('appId', this.addForm.appId)
+      addAppConfig(formData).then(resp => {
+        if (resp.code === 0) {
+          this.getData()
+        } else {
+          this.$message.info(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showAddDialog = false
+      })
+    },
+    onSelectChange() {
+      this.currentPage = 1
+      this.queryInfo.pn = 1
+      this.$router.push({
+        path: '/devops/app/config',
+        query: this.queryInfo
+      })
+      this.getData()
+    },
+    handleDelete(index, row) {
+      this.$confirm('确定要删除 ' + row.appName + '?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        const formData = new FormData()
+        formData.append('appId', row.appId)
+        deleteAppConfig(formData).then(resp => {
+          this.$message.info(resp.msg)
+          this.getData()
+        }).catch(error => {
+          this.$message.error(error.message)
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消'
+        })
+      })
+    },
+    handleErase(index, row) {
+      const formData = new FormData()
+      formData.append('appId', row.appId)
+      eraseAppRepo(formData).then(resp => {
+        this.$message.info(resp.msg)
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    // ****************************************************************************************************************
+    // 应用部署配置
+    // ****************************************************************************************************************
+    handleDeployConfig(index, row) {
+      this.addDeployForm.appId = row.appId
+      this.addDeployForm.packType = row.packType
+      getAppDeployConfigList(row.appId).then(resp => {
+        if (resp.code === 0) {
+          this.appDeployConfigList = resp.data
+          this.showDeployConfigDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleAddDeployConfig(index, row) {
+      getDeployMachineList(this.queryInfo.env).then(resp => {
+        if (resp.code === 0) {
+          this.machineList = resp.data
+          this.showAddDeployConfigDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    onAddDeploy() {
+      const formData = new FormData()
+      formData.append('appId', this.addDeployForm.appId)
+      addAppDeployConfig(formData).then(resp => {
+        this.$message.info(resp.msg)
+        this.getData()
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleEditDeployConfig(index, row) {
+      this.editDeployForm = row
+      this.showEditDeployConfigDialog = true
+    },
+    onEditDeploy() {
+      const formData = new FormData()
+      formData.append('appId', this.editDeployForm.appId)
+      updateAppDeployConfig(formData).then(resp => {
+        this.$message.info(resp.msg)
+        this.getData()
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleDeleteDeployConfig(index, row) {
+      this.$confirm('确定要删除 ' + row.appName + '?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        const formData = new FormData()
+        formData.append('appId', row.appId)
+        deleteAppDeployConfig(formData).then(resp => {
+          this.$message.info(resp.msg)
+          this.getData()
+        }).catch(error => {
+          this.$message.error(error.message)
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消'
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 307 - 0
src/views/devops/app/AppStat.vue

@@ -0,0 +1,307 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>应用运行列表</h3>
+      <el-row style="margin-top: 10px">
+        <el-select
+          v-model="queryInfo.env"
+          clearable
+          placeholder="环境"
+          style="margin-left: 5px"
+          @change="onSelectChange"
+        >
+          <el-option
+            v-for="(item, index) in envList"
+            :key="index"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+        <el-select
+          v-model="queryInfo.appType"
+          clearable
+          placeholder="类型"
+          style="margin-left: 5px"
+          @change="onSelectChange"
+        >
+          <el-option
+            v-for="(item, index) in appTypeList"
+            :key="index"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-row>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="appName"
+          label="应用"
+        />
+        <el-table-column
+          prop="bindPorts"
+          label="监听端口"
+        />
+        <el-table-column
+          prop="packagePath"
+          label="包路径"
+        />
+        <el-table-column
+          prop="totalDeployed"
+          label="部署数量/运行中/未运行"
+        >
+          <template slot-scope="scope">
+            <el-tag disable-transitions style="margin-top: 5px; margin-left: 5px">
+              {{ scope.row.totalDeployed }}
+            </el-tag>
+            <el-tag :type="'success'" disable-transitions style="margin-top: 5px; margin-left: 5px">
+              {{ scope.row.totalRunning }}
+            </el-tag>
+            <el-tag :type="'danger'" disable-transitions style="margin-top: 5px; margin-left: 5px">
+              {{ scope.row.totalStopped }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          fixed="right"
+          label="详情"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handleDetail(scope.$index, scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <el-dialog
+      title="应用状态列表"
+      append-to-body
+      :visible.sync="showStatDialog"
+      width="70%"
+      center
+    >
+      <template>
+        <el-table
+          :data="appStatList"
+          border
+          height="480"
+          style="width: 100%"
+        >
+          <el-table-column
+            prop="machineIpv4"
+            label="机器地址"
+          />
+          <el-table-column
+            prop="packagePath"
+            label="包路径"
+          />
+          <el-table-column
+            prop="status"
+            label="运行状态"
+          />
+          <el-table-column
+            prop="startTime"
+            label="启动时间"
+          />
+          <el-table-column
+            prop="pid"
+            label="PID"
+          />
+          <el-table-column
+            prop="lastCheck"
+            label="上次检查"
+          />
+          <el-table-column
+            fixed="right"
+            label="操作"
+            width="280"
+          >
+            <template slot-scope="scope">
+              <el-button
+                style="margin-top: 5px; margin-left: 5px"
+                size="mini"
+                type="danger"
+                @click="handleRestart(scope.$index, scope.row)"
+              >重启</el-button>
+              <el-button
+                style="margin-top: 5px; margin-left: 5px"
+                size="mini"
+                type="danger"
+                @click="handleStop(scope.$index, scope.row)"
+              >停止</el-button>
+              <el-button
+                style="margin-top: 5px; margin-left: 5px"
+                size="mini"
+                type="success"
+                @click="handleStart(scope.$index, scope.row)"
+              >启动</el-button>
+              <el-button
+                style="margin-top: 5px; margin-left: 5px"
+                size="mini"
+                @click="handleGetStat(scope.$index, scope.row)"
+              >当前状态</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getAppStat, getAppStatList, getAppTypeList, getEnvList } from '@/api/devops'
+
+export default {
+  name: 'AppStat',
+  data() {
+    return {
+      envList: [],
+      appTypeList: [],
+      queryInfo: {
+        env: 'test',
+        appType: 'java',
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showStatDialog: false,
+      appStatList: [],
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    const env = this.$route.query.env
+    if (env !== undefined && env !== null) {
+      this.queryInfo.env = env
+    }
+    const appType = this.$route.query.appType
+    if (appType !== undefined && appType !== null) {
+      this.queryInfo.appType = appType
+    }
+    const pageNumber = this.$route.query.pn
+    if (pageNumber !== undefined && pageNumber !== null) {
+      this.currentPage = parseInt(pageNumber)
+      this.queryInfo.pn = parseInt(pageNumber)
+    }
+
+    document.title = '运行状态'
+    getEnvList().then(resp => {
+      if (resp.code === 0) {
+        this.envList = resp.data
+      } else {
+        this.$message.error(resp.msg)
+      }
+    }).catch(error => {
+      this.$message.error(error.message)
+    })
+    getAppTypeList().then(resp => {
+      if (resp.code === 0) {
+        this.appTypeList = resp.data
+      } else {
+        this.$message.error(resp.msg)
+      }
+    }).catch(error => {
+      this.$message.error(error.message)
+    })
+
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.queryInfo.pn = pageNumber
+      this.$router.push({
+        path: '/devops/app/stat',
+        query: this.queryInfo
+      })
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getAppStatList(this.queryInfo).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleDetail(index, row) {
+      getAppStat(row.appId).then(resp => {
+        if (resp.code === 0) {
+          this.appStatList = resp.data
+          this.showStatDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleRestart(index, row) {
+      this.$message.info('handleRestart')
+    },
+    handleStop(index, row) {
+      this.$message.info('handleStop')
+    },
+    handleStart(index, row) {
+      this.$message.info('handleStart')
+    },
+    handleGetStat(index, row) {
+      this.$message.info('handleGetStat')
+    },
+    onSelectChange() {
+      this.currentPage = 1
+      this.queryInfo.pn = 1
+      this.$router.push({
+        path: '/devops/app/stat',
+        query: this.queryInfo
+      })
+      this.getData()
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 330 - 0
src/views/devops/app/BuildDeploy.vue

@@ -0,0 +1,330 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>构建部署列表</h3>
+      <el-row style="margin-top: 10px">
+        <el-select
+          v-model="queryInfo.env"
+          clearable
+          placeholder="环境"
+          style="margin-left: 5px"
+          @change="onSelectChange"
+        >
+          <el-option
+            v-for="(item, index) in envList"
+            :key="index"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+        <el-select
+          v-model="queryInfo.appType"
+          clearable
+          placeholder="类型"
+          style="margin-left: 5px"
+          @change="onSelectChange"
+        >
+          <el-option
+            v-for="(item, index) in appTypeList"
+            :key="index"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+        <el-button type="text" style="margin-left: 5px" @click="handleBuildTask">构建任务</el-button>
+        <el-button type="text" style="margin-left: 5px" @click="handleResetStat">重置状态</el-button>
+      </el-row>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="appName"
+          label="应用名"
+        />
+        <el-table-column
+          prop="appId"
+          label="应用 ID"
+        />
+        <el-table-column
+          prop="repoBranch"
+          label="分支"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column
+          prop="bindPorts"
+          label="监听端口"
+        />
+        <el-table-column
+          prop="commitId"
+          label="当前版本"
+        />
+        <el-table-column
+          prop="commitTime"
+          label="提交时间"
+        />
+        <el-table-column
+          prop="buildTime"
+          label="构建时间"
+        />
+        <el-table-column
+          prop="buildResult"
+          label="构建状态"
+        >
+          <template slot-scope="scope">
+            <el-button
+              v-if="scope.row.buildResult === '构建成功'"
+              size="mini"
+              type="success"
+              @click="handleResult(scope.$index, scope.row)"
+            >成功</el-button>
+            <el-button
+              v-else-if="scope.row.buildResult === '构建失败'"
+              size="mini"
+              type="danger"
+              @click="handleResult(scope.$index, scope.row)"
+            >失败</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="buildBy"
+          label="用户"
+        />
+        <el-table-column
+          prop="totalNode"
+          label="节点数/已运行"
+        >
+          <template slot-scope="scope">
+            <el-tag style="margin-top: 5px; margin-left: 5px" disable-transitions>
+              <span>{{ scope.row.totalNode }}</span>
+            </el-tag>
+            <el-tag style="margin-top: 5px; margin-left: 5px" disable-transitions>
+              <span>{{ scope.row.totalRunning }}</span>
+            </el-tag>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              type="success"
+              @click="handleNode(scope.$index, scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              @click="handleUpdateApp(scope.$index, scope.row)"
+            >更新</el-button>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              @click="handleBuildApp(scope.$index, scope.row)"
+            >构建</el-button>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              @click="handleDeploy(scope.$index, scope.row)"
+            >部署列表</el-button>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              @click="handleBuild(scope.$index, scope.row)"
+            >构建历史</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <el-dialog
+      title="构建任务"
+      append-to-body
+      :visible.sync="showTaskDialog"
+      center
+    >
+      <template>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="构建结果"
+      append-to-body
+      :visible.sync="showResultDialog"
+      center
+    >
+      <template>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="部署列表"
+      append-to-body
+      :visible.sync="showDeployDialog"
+      width="70%"
+      center
+    >
+      <template>
+      </template>
+    </el-dialog>
+    <el-dialog
+      title="构建历史列表"
+      append-to-body
+      :visible.sync="showBuildDialog"
+      width="70%"
+      center
+    >
+      <template>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import { getAppTypeList, getBuildDeployList, getEnvList } from '@/api/devops'
+
+export default {
+  name: 'BuildDeploy',
+  data() {
+    return {
+      envList: [],
+      appTypeList: [],
+      queryInfo: {
+        env: 'test',
+        appType: 'java',
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      // **********************************************************************
+      showTaskDialog: null,
+      showResultDialog: false,
+      showBuildDialog: false,
+      showDeployDialog: false
+    }
+  },
+  created() {
+    const env = this.$route.query.env
+    if (env !== undefined && env !== null) {
+      this.queryInfo.env = env
+    }
+    const appType = this.$route.query.appType
+    if (appType !== undefined && appType !== null) {
+      this.queryInfo.appType = appType
+    }
+    const pageNumber = this.$route.query.pn
+    if (pageNumber !== undefined && pageNumber !== null) {
+      this.currentPage = parseInt(pageNumber)
+      this.queryInfo.pn = parseInt(pageNumber)
+    }
+
+    document.title = '构建部署'
+    getEnvList().then(resp => {
+      if (resp.code === 0) {
+        this.envList = resp.data
+      } else {
+        this.$message.error(resp.msg)
+      }
+    }).catch(error => {
+      this.$message.error(error.message)
+    })
+    getAppTypeList().then(resp => {
+      if (resp.code === 0) {
+        this.appTypeList = resp.data
+      } else {
+        this.$message.error(resp.msg)
+      }
+    }).catch(error => {
+      this.$message.error(error.message)
+    })
+
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.queryInfo.pn = pageNumber
+      this.$router.push({
+        path: '/devops/app/bd',
+        query: this.queryInfo
+      })
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getBuildDeployList(this.queryInfo).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleBuildTask() {
+      this.showTaskDialog = true
+    },
+    handleResetStat() {
+      this.$message.info('handleResetStat')
+    },
+    handleBuild(index, row) {
+      this.showBuildDialog = true
+    },
+    handleDeploy(index, row) {
+      this.showDeployDialog = true
+    },
+    handleUpdateApp(index, row) {
+      this.$message.info('handleUpdateApp')
+    },
+    handleBuildApp() {
+      this.$message.info('handleBuildApp')
+    },
+    onSelectChange() {
+      this.currentPage = 1
+      this.queryInfo.pn = 1
+      this.$router.push({
+        path: '/devops/app/bd',
+        query: this.queryInfo
+      })
+      this.getData()
+    },
+    handleResult() {
+      this.showResultDialog = true
+    },
+    handleNode() {
+      this.$message.info('handleNode')
+      /* this.$router.push({
+        path: '/devops/app/stat',
+        query: this.queryInfo
+      })*/
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 87 - 0
src/views/devops/build/BuildDir.vue

@@ -0,0 +1,87 @@
+<template>
+  <el-container>
+    <el-header>
+      <h3>构建数据目录</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="machineIpv4"
+          label="机器地址"
+        />
+        <el-table-column
+          prop="dirPath"
+          label="本地目录"
+        />
+        <el-table-column
+          prop="mountedOn"
+          label="所属分区"
+        />
+        <el-table-column
+          prop="totalStr"
+          label="分区总量"
+        />
+        <el-table-column
+          prop="availStr"
+          label="分区可用"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >清空</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-main>
+  </el-container>
+</template>
+
+<script>
+import { eraseBuildDir, getBuildDir } from '@/api/devops'
+
+export default {
+  name: 'BuildDir',
+  data() {
+    return {
+      dataList: []
+    }
+  },
+  created() {
+    document.title = '构建目录'
+    this.getData()
+  },
+  methods: {
+    getData() {
+      this.dataList = []
+      getBuildDir().then(resp => {
+        if (resp.code === 0) {
+          this.dataList = resp.data
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    handleEdit(index, row) {
+      eraseBuildDir().then(resp => {
+        this.$message.info(resp.msg)
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 266 - 0
src/views/devops/build/Compiler.vue

@@ -0,0 +1,266 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>编译配置列表</h3>
+      <el-row style="margin-top: 10px">
+        <el-button type="success" size="mini" icon="el-icon-plus" @click="handleShowAdd">添加</el-button>
+      </el-row>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="type"
+          label="编译类型"
+        />
+        <el-table-column
+          prop="name"
+          label="编译名字"
+        />
+        <el-table-column
+          prop="homePath"
+          label="编译器主目录"
+        />
+        <el-table-column
+          prop="compilerImage"
+          label="编译器镜像"
+        />
+        <el-table-column
+          prop="compileCmd"
+          label="编译命令"
+        />
+        <el-table-column
+          prop="versionCmd"
+          label="编译器版本命令"
+        />
+        <el-table-column
+          prop="compilerBinds"
+          label="镜像映射"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="success"
+              @click="handleShowImage(scope.$index, scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="danger"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <!-- 添加编译器对话框 -->
+    <el-dialog
+      title="添加编译器"
+      append-to-body
+      :visible.sync="showAddDialog"
+      center
+    >
+      <template>
+        <el-form ref="form" :model="form" label-width="80px">
+          <el-form-item label="编译类型">
+            <el-select v-model="form.type" placeholder="选择编译类型">
+              <el-option
+                v-for="(item, index) in compileTypes"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="编译名字">
+            <el-input v-model="form.name" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="编译主目录" style="width: 70%; padding-right: 2px">
+            <el-input v-model="form.homePath" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="编译命令">
+            <el-input v-model="form.versionCmd" type="textarea" autosize style="padding-right: 1px;" />
+          </el-form-item>
+          <el-form-item label="编译器版本命令" style="width: 70%; padding-right: 2px">
+            <el-input v-model="form.versionCmd" type="textarea" autosize style="padding-right: 1px;" />
+          </el-form-item>
+          <el-form-item label="编译镜像">
+            <el-input v-model="form.compilerImage" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onAddCompiler">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+    <!-- 查看 docker 目录映射对话框 -->
+    <el-dialog
+      title="docker 目录映射"
+      append-to-body
+      :visible.sync="showImageDialog"
+      center
+    >
+      <template>
+        <el-table
+          :data="dockerBinds"
+          style="width: 100%"
+        >
+          <el-table-column
+            prop="hostPath"
+            label="host 路径"
+          />
+          <el-table-column
+            prop="containerPath"
+            label="container 路径"
+          />
+        </el-table>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import { addCompiler, deleteCompiler, getCompilerList, getCompilerTypes, getImageBindList } from '@/api/devops'
+
+export default {
+  name: 'Compiler',
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      // **********************************************************************
+      showAddDialog: false,
+      showImageDialog: false,
+      form: {
+        type: 'none',
+        name: '',
+        homePath: '',
+        compileCmd: '',
+        versionCmd: '',
+        compilerImage: ''
+      },
+      compileTypes: [],
+      dockerBinds: []
+    }
+  },
+  created() {
+    document.title = '编译器列表'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getCompilerList(this.currentPage).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleShowAdd(index, row) {
+      getCompilerTypes().then(resp => {
+        if (resp.code === 0) {
+          this.showAddDialog = true
+          this.compileTypes = resp.data
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    onAddCompiler() {
+      const formData = new FormData()
+      formData.append('type', this.form.type)
+      addCompiler(formData).then(resp => {
+        this.$message.info(resp.msg)
+        this.getData()
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showAddDialog = false
+      })
+    },
+    handleShowImage(index, row) {
+      const queryInfo = {}
+      queryInfo.id = row.id
+      getImageBindList(queryInfo).then(resp => {
+        if (resp.code === 0) {
+          this.dockerBinds = resp.data
+          this.showImageDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleEdit(index, row) {
+      this.$confirm('确定要删除 ' + row.machineIpv4 + '?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        const formData = new FormData()
+        formData.append('id', row.id)
+        deleteCompiler(formData).then(resp => {
+          this.$message.info(resp.msg)
+          this.getData()
+        }).catch(error => {
+          this.$message.error(error.message)
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消'
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 175 - 0
src/views/devops/build/DockerRegistry.vue

@@ -0,0 +1,175 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>docker 仓库列表</h3>
+      <el-row style="margin-top: 10px">
+        <el-button type="success" size="mini" icon="el-icon-plus" @click="handleShowAdd">添加</el-button>
+      </el-row>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="registryUrl"
+          label="仓库地址"
+        />
+        <el-table-column
+          prop="username"
+          label="帐号"
+        />
+        <el-table-column
+          prop="password"
+          label="密码"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="danger"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <el-dialog
+      title="添加 docker 仓库"
+      append-to-body
+      :visible.sync="showAddDialog"
+      center
+    >
+      <template>
+        <el-form ref="form" :model="form" label-width="80px">
+          <el-form-item label="仓库地址">
+            <el-input v-model="form.registryUrl" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="用户名" style="width: 70%; padding-right: 2px">
+            <el-input v-model="form.username" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="密码">
+            <el-input v-model="form.password" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onAddDockerRegistry">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import { addDockerRegistry, deleteDockerRegistry, getDockerRegistryList } from '@/api/devops'
+
+export default {
+  name: 'DockerRegistry',
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      // **********************************************************************
+      showAddDialog: false,
+      form: {
+        registryUrl: '',
+        username: '',
+        password: ''
+      }
+    }
+  },
+  created() {
+    document.title = 'docker 仓库列表'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getDockerRegistryList(this.currentPage).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleShowAdd(index, row) {
+      this.showAddDialog = true
+    },
+    onAddDockerRegistry() {
+      const formData = new FormData()
+      formData.append('registryUrl', this.form.registryUrl)
+      formData.append('username', this.form.username)
+      formData.append('password', this.form.password)
+      addDockerRegistry(formData).then(resp => {
+        this.$message.info(resp.msg)
+        this.getData()
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showAddDialog = false
+      })
+    },
+    handleEdit(index, row) {
+      this.$confirm('确定要删除 ' + row.name + '?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        const formData = new FormData()
+        formData.append('id', row.id)
+        deleteDockerRegistry(formData).then(resp => {
+          this.$message.info(resp.msg)
+          this.getData()
+        }).catch(error => {
+          this.$message.error(error.message)
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消'
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 210 - 0
src/views/devops/build/Packer.vue

@@ -0,0 +1,210 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>打包配置列表</h3>
+      <el-row style="margin-top: 10px">
+        <el-button type="success" size="mini" icon="el-icon-plus" @click="handleShowAdd">添加</el-button>
+      </el-row>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="type"
+          label="打包类型"
+        />
+        <el-table-column
+          prop="name"
+          label="打包名字"
+        />
+        <el-table-column
+          prop="targetPath"
+          label="存放位置"
+        />
+        <el-table-column
+          prop="binDirname"
+          label="bin 目录名"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="danger"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <el-dialog
+      title="添加打包配置"
+      append-to-body
+      :visible.sync="showAddDialog"
+      center
+    >
+      <template>
+        <el-form ref="form" :model="form" label-width="80px">
+          <el-form-item label="打包类型">
+            <el-select v-model="form.type" placeholder="选择打包类型">
+              <el-option
+                v-for="(item, index) in packTypes"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="docker 仓库">
+            <el-select v-model="form.dockerRegistry" placeholder="选择 docker 仓库">
+              <el-option
+                v-for="(item, index) in registryList"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="打包名字">
+            <el-input v-model="form.name" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="存放位置" style="width: 70%; padding-right: 2px">
+            <el-input v-model="form.targetPath" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onAddPacker">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import { addPacker, deletePacker, getPackerList, getPackTypes } from '@/api/devops'
+
+export default {
+  name: 'Packer',
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      // **********************************************************************
+      showAddDialog: false,
+      form: {
+        type: '',
+        name: '',
+        targetPath: '',
+        dockerRegistry: ''
+      },
+      packTypes: [],
+      registryList: []
+    }
+  },
+  created() {
+    document.title = '打包配置列表'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getPackerList(this.currentPage).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleShowAdd(index, row) {
+      getPackTypes().then(resp => {
+        if (resp.code === 0) {
+          this.showAddDialog = true
+          this.packTypes = resp.data.packTypes
+          this.registryList = resp.data.registryList
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    onAddPacker() {
+      const formData = new FormData()
+      formData.append('type', this.form.type)
+      formData.append('dockerRegistry', this.form.dockerRegistry)
+      formData.append('name', this.form.name)
+      formData.append('targetPath', this.form.targetPath)
+      addPacker(formData).then(resp => {
+        this.$message.info(resp.msg)
+        this.getData()
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showAddDialog = false
+      })
+    },
+    handleEdit(index, row) {
+      this.$confirm('确定要删除 ' + row.name + '?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        const formData = new FormData()
+        formData.append('id', row.id)
+        deletePacker(formData).then(resp => {
+          this.$message.info(resp.msg)
+          this.getData()
+        }).catch(error => {
+          this.$message.error(error.message)
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消'
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 224 - 0
src/views/devops/build/RepoAuth.vue

@@ -0,0 +1,224 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>仓库认证列表</h3>
+      <el-row style="margin-top: 10px">
+        <el-button type="success" size="mini" icon="el-icon-plus" @click="handleShowAdd">添加</el-button>
+      </el-row>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="type"
+          label="仓库类型"
+        />
+        <el-table-column
+          prop="name"
+          label="认证名字"
+        />
+        <el-table-column
+          prop="authType"
+          label="认证类型"
+        />
+        <el-table-column
+          prop="username"
+          label="帐号"
+        />
+        <el-table-column
+          prop="password"
+          label="密码"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="danger"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <!-- 添加编译器对话框 -->
+    <el-dialog
+      title="添加编译器"
+      append-to-body
+      :visible.sync="showAddDialog"
+      center
+    >
+      <template>
+        <el-form ref="form" :model="form" label-width="80px">
+          <el-form-item label="仓库类型">
+            <el-select v-model="form.type" placeholder="选择编译类型">
+              <el-option
+                v-for="(item, index) in repoTypes"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="认证类型">
+            <el-select v-model="form.authType" placeholder="选择编译类型">
+              <el-option
+                v-for="(item, index) in authTypes"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="认证名字">
+            <el-input v-model="form.name" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="用户名" style="width: 70%; padding-right: 2px">
+            <el-input v-model="form.username" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="密码">
+            <el-input v-model="form.password" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+<!--          <el-form-item label="私钥" style="width: 70%; padding-right: 2px">
+            <el-input v-model="form.rsaPrikey" type="textarea" autosize style="padding-right: 1px;" />
+          </el-form-item>-->
+          <el-form-item>
+            <el-button type="primary" @click="onAddRepoAuth">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import { addRepoAuth, deleteRepoAuth, getRepoAuthList, getRepoTypes } from '@/api/devops'
+
+export default {
+  name: 'RepoAuth',
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      // **********************************************************************
+      showAddDialog: false,
+      form: {
+        type: 'git',
+        name: '',
+        authType: 'http',
+        username: '',
+        password: '',
+        rsaPrikey: ''
+      },
+      repoTypes: [],
+      authTypes: []
+    }
+  },
+  created() {
+    document.title = '仓库认证列表'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getRepoAuthList(this.currentPage).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleShowAdd(index, row) {
+      getRepoTypes().then(resp => {
+        if (resp.code === 0) {
+          this.showAddDialog = true
+          this.repoTypes = resp.data.repoTypes
+          this.authTypes = resp.data.authTypes
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    onAddRepoAuth() {
+      const formData = new FormData()
+      formData.append('type', this.form.type)
+      formData.append('name', this.form.name)
+      formData.append('authType', this.form.authType)
+      formData.append('username', this.form.username)
+      formData.append('password', this.form.password)
+      addRepoAuth(formData).then(resp => {
+        this.$message.info(resp.msg)
+        this.getData()
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showAddDialog = false
+      })
+    },
+    handleEdit(index, row) {
+      this.$confirm('确定要删除 ' + row.name + '?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        const formData = new FormData()
+        formData.append('id', row.id)
+        deleteRepoAuth(formData).then(resp => {
+          this.$message.info(resp.msg)
+          this.getData()
+        }).catch(error => {
+          this.$message.error(error.message)
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消'
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 245 - 0
src/views/devops/file/FileList.vue

@@ -0,0 +1,245 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>我的文件</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          fixed="left"
+          label="No"
+          type="index"
+        />
+        <el-table-column
+          prop="pubDate"
+          label="文件名"
+          width="150"
+        />
+        <el-table-column
+          prop="videoId"
+          label="修改时间"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <router-link target="_blank" :to="`/video/${scope.row.videoId}`">
+              <span>{{ scope.row.videoId }}</span>
+            </router-link>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="description"
+          label="大小"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.description"
+              :content="scope.row.description"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.description && scope.row.description.length <= 15">
+                {{ scope.row.description }}
+              </span>
+              <span v-if="scope.row.description && scope.row.description.length > 15">
+                {{ scope.row.description.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+            <span v-else-if="scope.row.description === null">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="duration"
+          label="文件类型"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handlePreview(scope.$index, scope.row)"
+            >预览</el-button>
+            <el-button
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >编辑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 164 - 0
src/views/devops/file/ImageFile.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>图片列表</h3>
+    </el-header>
+    <el-main>
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 89 - 0
src/views/devops/machine/AliyunKey.vue

@@ -0,0 +1,89 @@
+<template>
+  <el-container>
+    <el-header>
+      <h3>阿里云帐号列表</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="type"
+          label="类型"
+        />
+        <el-table-column
+          prop="endpoint"
+          label="Endpoint"
+        />
+        <el-table-column
+          prop="accessKeyId"
+          label="AccessKeyId"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column
+          prop="accessKeySecret"
+          label="AccessKeySecret"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="danger"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-main>
+  </el-container>
+</template>
+
+<script>
+import { getAliyunKeyList } from '@/api/devops'
+
+export default {
+  name: 'AliyunKey',
+  data() {
+    return {
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: []
+    }
+  },
+  created() {
+    document.title = '阿里云帐号列表'
+    this.getData()
+  },
+  methods: {
+    getData() {
+      this.dataList = []
+      getAliyunKeyList().then(resp => {
+        if (resp.code === 0) {
+          this.dataList = resp.data
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleEdit(index, row) {
+      this.$message.info('delete ' + row.endpoint)
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 331 - 0
src/views/devops/machine/MachineHost.vue

@@ -0,0 +1,331 @@
+<template>
+  <el-container>
+    <el-header>
+      <el-row style="margin-top: 5px">
+        <el-select
+          v-model="queryInfo.env"
+          placeholder="查询条件"
+          style="margin-left: 5px"
+          @change="onSelectChange"
+        >
+          <el-option
+            v-for="(item, index) in envList"
+            :key="index"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-row>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="machineIpv4"
+          label="机器地址"
+        />
+        <el-table-column
+          prop="bootTime"
+          label="启动时间"
+        />
+        <el-table-column
+          prop="status"
+          label="当前状态"
+        >
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.status === 'Online'" :type="'success'" disable-transitions>
+              <span>在线</span>
+            </el-tag>
+            <el-tag v-else-if="scope.row.status === 'Offline'" :type="'danger'" disable-transitions>
+              <span>离线</span>
+            </el-tag>
+            <el-tag v-else :type="'warning'" disable-transitions>
+              <span>弃用</span>
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="osArch"
+          label="系统架构"
+        />
+        <el-table-column
+          prop="osName"
+          label="系统名字"
+        />
+        <el-table-column
+          prop="osVersion"
+          label="系统版本"
+        />
+        <el-table-column
+          prop="agentVersion"
+          label="Agent 版本"
+        />
+        <el-table-column
+          prop="used"
+          label="使用量"
+        >
+          <template slot-scope="scope">
+            <el-tag disable-transitions>
+              <span>{{ scope.row.used }}</span>
+            </el-tag>
+            <el-button
+              style="margin-top: 5px; margin-left: 5px"
+              size="mini"
+              type="success"
+              @click="handleMachineUsage(scope.$index, scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="duration"
+          label="所属环境"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="success"
+              @click="handleEditEnv(scope.$index, scope.row)"
+            >设置</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="操作"
+          width="180"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="warning"
+              @click="handleDeprecate(scope.$index, scope.row)"
+            >弃用</el-button>
+            <el-button
+              size="mini"
+              type="danger"
+              @click="handleDelete(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <!-- 修改所属环境对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditEnvDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改机器所属环境</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateEnv">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.env" placeholder="设置环境">
+            <el-option
+              v-for="(item, index) in envList"
+              :key="index"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <el-dialog
+      title="机器中部署的应用"
+      append-to-body
+      :visible.sync="showUsageDialog"
+      center
+    >
+      <template>
+        <el-table
+          :data="machineUsedList"
+          style="width: 100%"
+        >
+          <el-table-column
+            prop="label"
+            label="应用 ID"
+          />
+          <el-table-column
+            prop="value"
+            label="应用名"
+          />
+        </el-table>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import {
+  deleteMachine,
+  deprecateMachine,
+  getEnvList,
+  getMachineList,
+  getMachineUsedList,
+  updateMachineEnv
+} from '@/api/devops'
+
+export default {
+  name: 'MachineHost',
+  data() {
+    return {
+      queryInfo: {
+        pn: 1,
+        env: 'test'
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      showEditEnvDialog: false,
+      form: {
+        machineId: null,
+        env: null
+      },
+      envList: [],
+      showUsageDialog: false,
+      machineUsedList: []
+    }
+  },
+  created() {
+    const env = this.$route.query.env
+    if (env !== undefined && env !== null) {
+      this.queryInfo.env = env
+    }
+    const pageNumber = this.$route.query.pn
+    if (pageNumber !== undefined && pageNumber !== null) {
+      this.currentPage = pageNumber
+      this.queryInfo.pn = pageNumber
+    }
+
+    document.title = '机器列表'
+    getEnvList().then(resp => {
+      if (resp.code === 0) {
+        this.envList = resp.data
+        this.getData()
+      } else {
+        this.$message.error(resp.msg)
+      }
+    }).catch(error => {
+      this.$message.error(error.message)
+    })
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.queryInfo.pn = pageNumber
+      this.$router.push({
+        path: '/devops/machine/host',
+        query: this.queryInfo
+      })
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getMachineList(this.queryInfo).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleEditEnv(index, row) {
+      this.form.machineId = row.machineId
+      this.form.env = this.queryInfo.env
+      this.showEditEnvDialog = true
+    },
+    onUpdateEnv() {
+      this.showEditEnvDialog = false
+      const formData = new FormData()
+      formData.append('machineId', this.form.machineId)
+      formData.append('env', this.form.env)
+      updateMachineEnv(formData).then(resp => {
+        this.$message.info(resp.msg)
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleMachineUsage(index, row) {
+      this.form.machineId = row.machineId
+      getMachineUsedList(row.machineId).then(resp => {
+        if (resp.code === 0) {
+          this.showUsageDialog = true
+          this.machineUsedList = resp.data
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    onSelectChange() {
+      this.currentPage = 1
+      this.queryInfo.pn = 1
+      this.$router.push({
+        path: '/devops/machine/host',
+        query: this.queryInfo
+      })
+      this.getData()
+    },
+    handleDeprecate(index, row) {
+      const formData = new FormData()
+      formData.append('machineId', row.machineId)
+      deprecateMachine(formData).then(resp => {
+        this.$message.info(resp.msg)
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleDelete(index, row) {
+      this.$confirm('确定要删除 ' + row.machineIpv4 + '?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        const formData = new FormData()
+        formData.append('machineId', row.machineId)
+        deleteMachine(formData).then(resp => {
+          this.$message.info(resp.msg)
+        }).catch(error => {
+          this.$message.error(error.message)
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消'
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 163 - 0
src/views/devops/rbac/Menu.vue

@@ -0,0 +1,163 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>资源目录树</h3>
+    </el-header>
+    <el-main>
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getMenuList } from '@/api/devops'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'Menu'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getMenuList().then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 270 - 0
src/views/devops/rbac/Role.vue

@@ -0,0 +1,270 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>角色列表</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          fixed="left"
+          label="No"
+          type="index"
+        />
+        <el-table-column
+          prop="pubDate"
+          label="角色"
+          width="150"
+        />
+        <el-table-column
+          prop="videoId"
+          label="描述"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <router-link target="_blank" :to="`/video/${scope.row.videoId}`">
+              <span>{{ scope.row.videoId }}</span>
+            </router-link>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="title"
+          label="创建时间"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.title"
+              :content="scope.row.title"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.title.length <= 15">
+                {{ scope.row.title }}
+              </span>
+              <span v-else>
+                {{ scope.row.title.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="description"
+          label="拥有角色的用户"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.description"
+              :content="scope.row.description"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.description && scope.row.description.length <= 15">
+                {{ scope.row.description }}
+              </span>
+              <span v-if="scope.row.description && scope.row.description.length > 15">
+                {{ scope.row.description.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+            <span v-else-if="scope.row.description === null">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="horizontal"
+          label="授权资源"
+        >
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.horizontal" :type="'warning'" disable-transitions>
+              <span icon="el-icon-monitor">横屏</span>
+            </el-tag>
+            <el-tag v-else :type="'success'" disable-transitions>
+              <span icon="el-icon-mobile-phone">竖屏</span>
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getRoleList } from '@/api/devops'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'Role'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getRoleList().then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.data
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 292 - 0
src/views/devops/rbac/User.vue

@@ -0,0 +1,292 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>用户列表</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          fixed="left"
+          label="No"
+          type="index"
+        />
+        <el-table-column
+          prop="pubDate"
+          label="帐号"
+          width="150"
+        />
+        <el-table-column
+          prop="videoId"
+          label="创建时间"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <router-link target="_blank" :to="`/video/${scope.row.videoId}`">
+              <span>{{ scope.row.videoId }}</span>
+            </router-link>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="title"
+          label="会话总数"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.title"
+              :content="scope.row.title"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.title.length <= 15">
+                {{ scope.row.title }}
+              </span>
+              <span v-else>
+                {{ scope.row.title.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="description"
+          label="最近访问"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.description"
+              :content="scope.row.description"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.description && scope.row.description.length <= 15">
+                {{ scope.row.description }}
+              </span>
+              <span v-if="scope.row.description && scope.row.description.length > 15">
+                {{ scope.row.description.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+            <span v-else-if="scope.row.description === null">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="duration"
+          label="状态"
+        />
+        <el-table-column
+          prop="horizontal"
+          label="角色总数"
+        >
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.horizontal" :type="'warning'" disable-transitions>
+              <span icon="el-icon-monitor">横屏</span>
+            </el-tag>
+            <el-tag v-else :type="'success'" disable-transitions>
+              <span icon="el-icon-mobile-phone">竖屏</span>
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="scope"
+          label="修改密码"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.scope === 1" :type="'warning'" disable-transitions>
+              本人可见
+            </el-tag>
+            <el-tag v-if="scope.row.scope === 2" :type="'success'" disable-transitions>
+              所有人可见
+            </el-tag>
+            <el-tag v-if="scope.row.scope === 3" :type="'danger'" disable-transitions>
+              VIP 可见
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getUserList, getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getUserList().then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 164 - 0
src/views/devops/sys/AccessLog.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>访问日志</h3>
+    </el-header>
+    <el-main>
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 164 - 0
src/views/devops/sys/RealtimeLog.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>实时日志</h3>
+    </el-header>
+    <el-main>
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 164 - 0
src/views/devops/sys/RuntimeLog.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>运行访问日志</h3>
+    </el-header>
+    <el-main>
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 219 - 0
src/views/devops/sys/SiteConfig.vue

@@ -0,0 +1,219 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>站点配置</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          fixed="left"
+          label="No"
+          type="index"
+        />
+        <el-table-column
+          prop="pubDate"
+          label="属性名"
+          width="150"
+        />
+        <el-table-column
+          prop="description"
+          label="描述"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.description"
+              :content="scope.row.description"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.description && scope.row.description.length <= 15">
+                {{ scope.row.description }}
+              </span>
+              <span v-if="scope.row.description && scope.row.description.length > 15">
+                {{ scope.row.description.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+            <span v-else-if="scope.row.description === null">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="duration"
+          label="属性值"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handlePreview(scope.$index, scope.row)"
+            >更新</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 223 - 0
src/views/devops/sys/Webhook.vue

@@ -0,0 +1,223 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>webhook 通知</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          fixed="left"
+          label="No"
+          type="index"
+        />
+        <el-table-column
+          prop="pubDate"
+          label="发送帐号"
+          width="150"
+        />
+        <el-table-column
+          prop="videoId"
+          label="URL"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <router-link target="_blank" :to="`/video/${scope.row.videoId}`">
+              <span>{{ scope.row.videoId }}</span>
+            </router-link>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="duration"
+          label="签名"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >测试</el-button>
+            <el-button
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 251 - 0
src/views/devops/user/UserLogin.vue

@@ -0,0 +1,251 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>登入记录</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          fixed="left"
+          label="No"
+          type="index"
+        />
+        <el-table-column
+          prop="pubDate"
+          label="登入时间"
+          width="150"
+        />
+        <el-table-column
+          prop="videoId"
+          label="登入 IP"
+          width="120"
+        >
+          <template slot-scope="scope">
+            <router-link target="_blank" :to="`/video/${scope.row.videoId}`">
+              <span>{{ scope.row.videoId }}</span>
+            </router-link>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="title"
+          label="登入位置"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.title"
+              :content="scope.row.title"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.title.length <= 15">
+                {{ scope.row.title }}
+              </span>
+              <span v-else>
+                {{ scope.row.title.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="description"
+          label="登入设备"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.description"
+              :content="scope.row.description"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.description && scope.row.description.length <= 15">
+                {{ scope.row.description }}
+              </span>
+              <span v-if="scope.row.description && scope.row.description.length > 15">
+                {{ scope.row.description.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+            <span v-else-if="scope.row.description === null">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="duration"
+          label="状态"
+        />
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >登出</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 229 - 0
src/views/devops/user/UserMessage.vue

@@ -0,0 +1,229 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>我的消息</h3>
+    </el-header>
+    <el-main>
+      <el-table
+        :data="dataList"
+        border
+        height="480"
+        style="width: 100%"
+      >
+        <el-table-column
+          fixed="left"
+          label="No"
+          type="index"
+        />
+        <el-table-column
+          prop="pubDate"
+          label="时间"
+          width="150"
+        />
+        <el-table-column
+          prop="title"
+          label="标题"
+          :show-overflow-tooltip="true"
+        >
+          <template slot-scope="scope">
+            <el-tooltip
+              v-if="scope.row.title"
+              :content="scope.row.title"
+              raw-content
+              placement="top-start"
+            >
+              <span v-if="scope.row.title.length <= 15">
+                {{ scope.row.title }}
+              </span>
+              <span v-else>
+                {{ scope.row.title.substr(0, 15) + "..." }}
+              </span>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="280"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handlePreview(scope.$index, scope.row)"
+            >查看</el-button>
+            <el-button
+              size="mini"
+              @click="handleEdit(scope.$index, scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        :small="screenWidth <= 768"
+        layout="prev, pager, next"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :total="totalSize"
+        @current-change="handleCurrentChange"
+        @prev-click="handleCurrentChange"
+        @next-click="handleCurrentChange"
+      />
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 164 - 0
src/views/devops/user/UserProfile.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-container>
+    <el-header height="220">
+      <h3>个人资料</h3>
+    </el-header>
+    <el-main>
+    </el-main>
+
+    <!-- 修改视频可见范围对话框 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="showEditScopeDialog"
+      width="30%"
+      center
+    >
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>修改视频可见范围</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="onUpdateScope">更新</el-button>
+        </div>
+        <div class="text item">
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="本人可见" value="1" />
+            <el-option label="所有人可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+          </el-select>
+        </div>
+      </el-card>
+    </el-dialog>
+    <!-- 视频预览对话框 -->
+    <el-dialog
+      title="预览视频"
+      append-to-body
+      :visible.sync="showPreviewDialog"
+      :before-close="handleDialogClose"
+      width="70%"
+      center
+    >
+      <template>
+        <video-preview-player :video-prop.sync="videoProp" />
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script>
+import VideoPreviewPlayer from 'components/VideoPreviewPlayer'
+import { updateVideoScope, videoInfo } from '@/api/video'
+import { getVideoList } from '@/api/admin'
+
+export default {
+  name: 'VideoPost',
+  components: { VideoPreviewPlayer },
+  data() {
+    return {
+      queryInfo: {
+        scope: null,
+        pn: 1
+      },
+      // 屏幕宽度, 为了控制分页条的大小
+      screenWidth: document.body.clientWidth,
+      currentPage: 1,
+      pageSize: 10,
+      totalSize: 0,
+      dataList: [],
+      nextId: 0,
+      // **********************************************************************
+      videoProp: null,
+      showVideoResourceDialog: false,
+      showEditScopeDialog: false,
+      showPreviewDialog: false,
+      form: {
+        videoId: null,
+        scope: 1
+      },
+      videoResources: [],
+      publishVideoDiaglog: false
+    }
+  },
+  created() {
+    document.title = 'AdminVideoList'
+    this.getData()
+  },
+  methods: {
+    handleCurrentChange(pageNumber) {
+      this.currentPage = pageNumber
+      this.getData()
+      // 回到顶部
+      scrollTo(0, 0)
+    },
+    getData() {
+      this.dataList = []
+      getVideoList(0).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data
+          this.dataList = respData.list
+          this.totalSize = respData.totalSize
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    onRefresh() {
+      this.getData()
+    },
+    handleScope(index, row) {
+      this.form.videoId = row.videoId
+      this.form.scope = '' + row.scope
+      this.showEditScopeDialog = true
+    },
+    handleDialogClose(done) {
+      this.showPreviewDialog = false
+      this.videoProp = {
+        videoId: null,
+        play: false
+      }
+      done()
+    },
+    handlePreview(index, row) {
+      videoInfo(row.videoId).then(res => {
+        if (res.code === 0) {
+          this.showPreviewDialog = true
+          this.videoProp = {
+            videoId: res.data.videoId,
+            play: true
+          }
+        }
+      })
+    },
+    handleEdit(index, row) {
+      const path = '/post/video/edit/' + row.videoId
+      this.$router.push(path)
+    },
+    onUpdateScope() {
+      this.showEditScopeDialog = false
+      updateVideoScope(this.form).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '提示',
+            message: '视频可见范围已更新',
+            type: 'warning',
+            duration: 3000
+          })
+        }
+      }).catch(error => {
+        this.$notify({
+          title: '提示',
+          message: error.message,
+          type: 'warning',
+          duration: 3000
+        })
+      })
+    },
+    onSelectChange() {
+      this.$message.info(this.queryInfo)
+    },
+    handleClose() {
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 5 - 0
src/views/my/MyProfile.vue

@@ -183,6 +183,7 @@ import { userMixin } from 'assets/js/mixin'
 import { updateAvatar } from '@/api/account'
 import { getAuthedUser, updateAuthedUser } from '@/utils/auth'
 import { getAvatarChannelInfo } from '@/api/file'
+import { getBlogPosts } from '@/api/devops'
 
 export default {
   name: 'MyProfile',
@@ -223,6 +224,10 @@ export default {
   },
   created() {
     this.loginUser = getAuthedUser()
+    getBlogPosts().then(resp => {
+      console.log(resp.data)
+    })
+
     getAvatarChannelInfo().then(res => {
       if (res.code === 0) {
         const resData = res.data