✨ host ssh数据结构调整
This commit is contained in:
parent
eaa5e5e65d
commit
5c3818dd73
@ -29,7 +29,6 @@
|
|||||||
"socket.io": "^4.7.5"
|
"socket.io": "^4.7.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^9.6.0",
|
|
||||||
"nodemon": "^3.1.4",
|
"nodemon": "^3.1.4",
|
||||||
"pkg": "5.8"
|
"pkg": "5.8"
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
// 规则参见:https://cn.eslint.org/docs/rules/
|
// 规则参见:https://cn.eslint.org/docs/rules/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true, // 当前配置文件不能往父级查找
|
root: true, // 当前配置文件不能往父级查找
|
||||||
'globals': {
|
'globals': { 'consola': true },
|
||||||
'consola': true
|
|
||||||
},
|
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
es6: true
|
es6: true
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils')
|
const { readHostList, writeHostList } = require('../utils')
|
||||||
|
|
||||||
async function getHostList({ res }) {
|
async function getHostList({ res }) {
|
||||||
// console.log('get-host-list')
|
// console.log('get-host-list')
|
||||||
@ -7,30 +7,52 @@ async function getHostList({ res }) {
|
|||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveHost({ res, request }) {
|
async function saveHost({
|
||||||
let { body: { host: newHost, name, index, expired, expiredNotify, group, consoleUrl, remark } } = request
|
res, request
|
||||||
|
}) {
|
||||||
|
let {
|
||||||
|
body: {
|
||||||
|
name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
|
port, username, authType, password, privateKey, command
|
||||||
|
}
|
||||||
|
} = request
|
||||||
// console.log(request)
|
// console.log(request)
|
||||||
if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
|
if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
|
||||||
let hostList = await readHostList()
|
let hostList = await readHostList()
|
||||||
if (hostList?.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
if (hostList?.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
||||||
if (!Array.isArray(hostList)) hostList = []
|
if (!Array.isArray(hostList)) hostList = []
|
||||||
hostList.push({ host: newHost, name, index, expired, expiredNotify, group, consoleUrl, remark })
|
hostList.push({
|
||||||
|
host: newHost, name, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
|
port, username, authType, password, privateKey, command
|
||||||
|
})
|
||||||
await writeHostList(hostList)
|
await writeHostList(hostList)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateHost({ res, request }) {
|
async function updateHost({
|
||||||
let { body: { host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark } } = request
|
res, request
|
||||||
|
}) {
|
||||||
|
let {
|
||||||
|
body: {
|
||||||
|
host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
|
port, username, authType, password, privateKey, command
|
||||||
|
}
|
||||||
|
} = request
|
||||||
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||||
let hostList = await readHostList()
|
let hostList = await readHostList()
|
||||||
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
|
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试新增实例` })
|
||||||
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
||||||
hostList.splice(targetIdx, 1, { name: newName, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark })
|
hostList.splice(targetIdx, 1, {
|
||||||
|
name: newName, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
|
port, username, authType, password, privateKey, command
|
||||||
|
})
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeHost({ res, request }) {
|
async function removeHost({
|
||||||
|
res, request
|
||||||
|
}) {
|
||||||
let { body: { host } } = request
|
let { body: { host } } = request
|
||||||
let hostList = await readHostList()
|
let hostList = await readHostList()
|
||||||
let hostIdx = hostList.findIndex(item => item.host === host)
|
let hostIdx = hostList.findIndex(item => item.host === host)
|
||||||
@ -38,13 +60,14 @@ async function removeHost({ res, request }) {
|
|||||||
hostList.splice(hostIdx, 1)
|
hostList.splice(hostIdx, 1)
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
// 查询是否存在ssh记录
|
// 查询是否存在ssh记录
|
||||||
let sshRecord = await readSSHRecord()
|
// let sshRecord = await readSSHRecord()
|
||||||
let sshIdx = sshRecord.findIndex(item => item.host === host)
|
// let sshIdx = sshRecord.findIndex(item => item.host === host)
|
||||||
let flag = sshIdx !== -1
|
// let flag = sshIdx !== -1
|
||||||
if (flag) sshRecord.splice(sshIdx, 1)
|
// if (flag) sshRecord.splice(sshIdx, 1)
|
||||||
writeSSHRecord(sshRecord)
|
// writeSSHRecord(sshRecord)
|
||||||
|
|
||||||
res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` })
|
// res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` })
|
||||||
|
res.success({ data: `${ host }已移除` })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原手动排序接口-废弃
|
// 原手动排序接口-废弃
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
const { readSSHRecord, writeSSHRecord, AESEncryptSync } = require('../utils')
|
const { readSSHRecord, writeSSHRecord, AESEncryptSync } = require('../utils')
|
||||||
|
|
||||||
|
async function getSSHList({ res }) {
|
||||||
|
// console.log('get-host-list')
|
||||||
|
let data = await readSSHRecord()
|
||||||
|
data = data?.map(item => {
|
||||||
|
const { host, port, username, _id } = item
|
||||||
|
return { host, port, username, _id }
|
||||||
|
}) || []
|
||||||
|
res.success({ data })
|
||||||
|
}
|
||||||
|
|
||||||
const updateSSH = async ({ res, request }) => {
|
const updateSSH = async ({ res, request }) => {
|
||||||
let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
|
let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
|
||||||
let record = { host, port, username, type, password, privateKey, randomKey, command }
|
let record = { host, port, username, type, password, privateKey, randomKey, command }
|
||||||
@ -50,6 +60,7 @@ const getCommand = async ({ res, request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
getSSHList,
|
||||||
updateSSH,
|
updateSSH,
|
||||||
removeSSH,
|
removeSSH,
|
||||||
existSSH,
|
existSSH,
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
const { updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh')
|
const { getSSHList, updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh')
|
||||||
const { getHostList, saveHost, updateHost, removeHost } = require('../controller/host')
|
const { getHostList, saveHost, updateHost, removeHost } = require('../controller/host')
|
||||||
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
||||||
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
||||||
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
||||||
|
|
||||||
const ssh = [
|
const ssh = [
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/get-ssh-list',
|
||||||
|
controller: getSSHList
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/update-ssh',
|
path: '/update-ssh',
|
||||||
|
@ -7,6 +7,9 @@ export default {
|
|||||||
getIpInfo (params = {}) {
|
getIpInfo (params = {}) {
|
||||||
return axios({ url: '/ip-info', method: 'get', params })
|
return axios({ url: '/ip-info', method: 'get', params })
|
||||||
},
|
},
|
||||||
|
getSSHList(params = {}) {
|
||||||
|
return axios({ url: '/get-ssh-list', method: 'get', params })
|
||||||
|
},
|
||||||
updateSSH(data) {
|
updateSSH(data) {
|
||||||
return axios({ url: '/update-ssh', method: 'post', data })
|
return axios({ url: '/update-ssh', method: 'post', data })
|
||||||
},
|
},
|
||||||
|
@ -39,7 +39,7 @@ import { useRoute } from 'vue-router'
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const { proxy: { $router, $route, $store, $message } } = getCurrentInstance()
|
const { proxy: { $router, $store } } = getCurrentInstance()
|
||||||
|
|
||||||
let menuList = reactive([
|
let menuList = reactive([
|
||||||
{
|
{
|
||||||
@ -52,11 +52,11 @@ let menuList = reactive([
|
|||||||
icon: markRaw(ScaleToOriginal),
|
icon: markRaw(ScaleToOriginal),
|
||||||
index: '/terminal'
|
index: '/terminal'
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: '凭据管理',
|
// name: '凭据管理',
|
||||||
icon: markRaw(Key),
|
// icon: markRaw(Key),
|
||||||
index: '/credentials'
|
// index: '/credentials'
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
name: '分组管理',
|
name: '分组管理',
|
||||||
icon: markRaw(FolderOpened),
|
icon: markRaw(FolderOpened),
|
||||||
|
@ -7,6 +7,7 @@ const useStore = defineStore({
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
hostList: [],
|
hostList: [],
|
||||||
groupList: [],
|
groupList: [],
|
||||||
|
sshList: [],
|
||||||
user: localStorage.getItem('user') || null,
|
user: localStorage.getItem('user') || null,
|
||||||
token: sessionStorage.getItem('token') || localStorage.getItem('token') || null,
|
token: sessionStorage.getItem('token') || localStorage.getItem('token') || null,
|
||||||
title: ''
|
title: ''
|
||||||
@ -32,10 +33,11 @@ const useStore = defineStore({
|
|||||||
async getMainData() {
|
async getMainData() {
|
||||||
const { data: groupList } = await $api.getGroupList()
|
const { data: groupList } = await $api.getGroupList()
|
||||||
const { data: hostList } = await $api.getHostList()
|
const { data: hostList } = await $api.getHostList()
|
||||||
// const { data: sshList } = await $api.getSshList()
|
const { data: sshList } = await $api.getSSHList()
|
||||||
// console.log('hostList:', hostList)
|
// console.log('hostList:', hostList)
|
||||||
// console.log('groupList:', groupList)
|
// console.log('groupList:', groupList)
|
||||||
this.$patch({ hostList, groupList })
|
// console.log('sshList:', sshList)
|
||||||
|
this.$patch({ groupList, hostList, sshList })
|
||||||
},
|
},
|
||||||
async getHostList() {
|
async getHostList() {
|
||||||
const { data: hostList } = await $api.getHostList()
|
const { data: hostList } = await $api.getHostList()
|
||||||
|
@ -37,21 +37,21 @@
|
|||||||
<el-alert type="success" :closable="false">
|
<el-alert type="success" :closable="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span style="letter-spacing: 2px;">
|
<span style="letter-spacing: 2px;">
|
||||||
Tips: 已添加服务器数量 <u>{{ hostGroupInfo.total }}</u>
|
Tips: 已添加实例数量 <u>{{ hostGroupInfo.total }}</u>
|
||||||
<span v-show="hostGroupInfo.notGroupCount">, 有 <u>{{ hostGroupInfo.notGroupCount }}</u> 台服务器尚未分组</span>
|
<span v-show="hostGroupInfo.notGroupCount">, 有 <u>{{ hostGroupInfo.notGroupCount }}</u> 台实例尚未分组</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-alert><br>
|
</el-alert><br>
|
||||||
<el-alert type="success" :closable="false">
|
<el-alert type="success" :closable="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span style="letter-spacing: 2px;"> Tips: 删除分组会将分组内所有服务器移至默认分组 </span>
|
<span style="letter-spacing: 2px;"> Tips: 删除分组会将分组内所有实例移至默认分组 </span>
|
||||||
</template>
|
</template>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<el-table v-loading="loading" :data="list">
|
<el-table v-loading="loading" :data="list">
|
||||||
<el-table-column prop="index" label="序号" />
|
<el-table-column prop="index" label="序号" />
|
||||||
<el-table-column prop="id" label="ID" />
|
<el-table-column prop="id" label="ID" />
|
||||||
<el-table-column prop="name" label="分组名称" />
|
<el-table-column prop="name" label="分组名称" />
|
||||||
<el-table-column label="关联服务器数量">
|
<el-table-column label="关联实例数量">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-popover
|
<el-popover
|
||||||
v-if="row.hosts.list.length !== 0"
|
v-if="row.hosts.list.length !== 0"
|
||||||
|
@ -1,390 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-card shadow="always" class="host-card">
|
|
||||||
<div class="host-state">
|
|
||||||
<span v-if="isError" class="offline">未连接</span>
|
|
||||||
<span v-else class="online">已连接 {{ ping }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
<div class="weizhi field">
|
|
||||||
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
|
||||||
<template #reference>
|
|
||||||
<svg-icon name="icon-fuwuqi" class="svg-icon" />
|
|
||||||
</template>
|
|
||||||
<div class="field-detail">
|
|
||||||
<h2>系统</h2>
|
|
||||||
<h3><span>名称:</span> {{ osInfo.hostname }}</h3>
|
|
||||||
<h3><span>类型:</span> {{ osInfo.type }}</h3>
|
|
||||||
<h3><span>架构:</span> {{ osInfo.arch }}</h3>
|
|
||||||
<h3><span>平台:</span> {{ osInfo.platform }}</h3>
|
|
||||||
<h3><span>版本:</span> {{ osInfo.release }}</h3>
|
|
||||||
<h3><span>开机时长:</span> {{ $tools.formatTime(osInfo.uptime) }}</h3>
|
|
||||||
<h3><span>到期时间:</span> {{ expiredTime }}</h3>
|
|
||||||
<h3><span>本地IP:</span> {{ osInfo.ip }}</h3>
|
|
||||||
<h3><span>连接数:</span> {{ openedCount || 0 }}</h3>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
<div class="fields">
|
|
||||||
<span class="name" @click="handleUpdate">
|
|
||||||
{{ name || '--' }}
|
|
||||||
<svg-icon name="icon-xiugai" class="svg-icon" />
|
|
||||||
</span>
|
|
||||||
<span>{{ osInfo?.type || '--' }}</span>
|
|
||||||
<!-- <span>{{ osInfo?.hostname || '--' }}</span> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="weizhi field">
|
|
||||||
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
|
||||||
<template #reference>
|
|
||||||
<svg-icon name="icon-position" class="svg-icon" />
|
|
||||||
</template>
|
|
||||||
<div class="field-detail">
|
|
||||||
<h2>位置信息</h2>
|
|
||||||
<h3><span>详细:</span> {{ ipInfo.country || '--' }} {{ ipInfo.regionName }}</h3>
|
|
||||||
<!-- <h3><span>详细:</span> {{ ipInfo.country || '--' }} {{ ipInfo.regionName }} {{ ipInfo.city }}</h3> -->
|
|
||||||
<!-- <h3><span>IP:</span> {{ hostIp }}</h3> -->
|
|
||||||
<h3><span>提供商:</span> {{ ipInfo.isp || '--' }}</h3>
|
|
||||||
<h3><span>线路:</span> {{ ipInfo.as || '--' }}</h3>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
<div class="fields">
|
|
||||||
<span>{{ `${ipInfo?.country || '--'} ${ipInfo?.regionName || '--'}` }}</span>
|
|
||||||
<!-- <span>{{ `${ipInfo?.country || '--' } ${ipInfo?.regionName || '--'} ${ipInfo?.city || '--'}` }}</span> -->
|
|
||||||
<span>{{ hostIp }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cpu field">
|
|
||||||
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
|
||||||
<template #reference>
|
|
||||||
<svg-icon name="icon-xingzhuang" class="svg-icon" />
|
|
||||||
</template>
|
|
||||||
<div class="field-detail">
|
|
||||||
<h2>CPU</h2>
|
|
||||||
<h3><span>利用率:</span> {{ cpuInfo.cpuUsage }}%</h3>
|
|
||||||
<h3><span>物理核心:</span> {{ cpuInfo.cpuCount }}</h3>
|
|
||||||
<h3><span>型号:</span> {{ cpuInfo.cpuModel }}</h3>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
<div class="fields">
|
|
||||||
<span :style="{ color: setColor(cpuInfo.cpuUsage) }">{{ cpuInfo.cpuUsage || '0' || '--' }}%</span>
|
|
||||||
<span>{{ cpuInfo.cpuCount || '--' }} 核心</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ram field">
|
|
||||||
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
|
||||||
<template #reference>
|
|
||||||
<svg-icon name="icon-neicun1" class="svg-icon" />
|
|
||||||
</template>
|
|
||||||
<div class="field-detail">
|
|
||||||
<h2>内存</h2>
|
|
||||||
<h3><span>总大小:</span> {{ $tools.toFixed(memInfo.totalMemMb / 1024) }} GB</h3>
|
|
||||||
<h3><span>已使用:</span> {{ $tools.toFixed(memInfo.usedMemMb / 1024) }} GB</h3>
|
|
||||||
<h3><span>占比:</span> {{ $tools.toFixed(memInfo.usedMemPercentage) }}%</h3>
|
|
||||||
<h3><span>空闲:</span> {{ $tools.toFixed(memInfo.freeMemMb / 1024) }} GB</h3>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
<div class="fields">
|
|
||||||
<span :style="{ color: setColor(memInfo.usedMemPercentage) }">{{ $tools.toFixed(memInfo.usedMemPercentage)
|
|
||||||
}}%</span>
|
|
||||||
<span>{{ $tools.toFixed(memInfo.usedMemMb / 1024) }} | {{ $tools.toFixed(memInfo.totalMemMb / 1024) }} GB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="yingpan field">
|
|
||||||
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
|
||||||
<template #reference>
|
|
||||||
<svg-icon name="icon-xingzhuang1" class="svg-icon" />
|
|
||||||
</template>
|
|
||||||
<div class="field-detail">
|
|
||||||
<h2>存储</h2>
|
|
||||||
<h3><span>总空间:</span> {{ driveInfo.totalGb || '--' }} GB</h3>
|
|
||||||
<h3><span>已使用:</span> {{ driveInfo.usedGb || '--' }} GB</h3>
|
|
||||||
<h3><span>剩余:</span> {{ driveInfo.freeGb || '--' }} GB</h3>
|
|
||||||
<h3><span>占比:</span> {{ driveInfo.usedPercentage || '--' }}%</h3>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
<div class="fields">
|
|
||||||
<span :style="{ color: setColor(driveInfo.usedPercentage) }">{{ driveInfo.usedPercentage || '--' }}%</span>
|
|
||||||
<span>{{ driveInfo.usedGb || '--' }} | {{ driveInfo.totalGb || '--' }} GB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="wangluo field">
|
|
||||||
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
|
||||||
<template #reference>
|
|
||||||
<svg-icon name="icon-wangluo1" class="svg-icon" />
|
|
||||||
</template>
|
|
||||||
<div class="field-detail">
|
|
||||||
<h2>网卡</h2>
|
|
||||||
<!-- <h3>
|
|
||||||
<span>实时流量</span>
|
|
||||||
<div>↑ {{ $tools.toFixed(netstatInfo.netTotal?.outputMb) || 0 }}MB / s</div>
|
|
||||||
<div>↓ {{ $tools.toFixed(netstatInfo.netTotal?.inputMb) || 0 }}MB / s</div>
|
|
||||||
</h3> -->
|
|
||||||
<div v-for="(value, key) in netstatInfo.netCards" :key="key" style="display: flex; flex-direction: column;">
|
|
||||||
<h3>
|
|
||||||
<span>{{ key }}</span>
|
|
||||||
<div>↑ {{ $tools.formatNetSpeed(value?.outputMb) || 0 }}</div>
|
|
||||||
<div>↓ {{ $tools.formatNetSpeed(value?.inputMb) || 0 }}</div>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
<div class="fields">
|
|
||||||
<span>↑ {{ $tools.formatNetSpeed(netstatInfo.netTotal?.outputMb) || 0 }}</span>
|
|
||||||
<span>↓ {{ $tools.formatNetSpeed(netstatInfo.netTotal?.inputMb) || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fields terminal">
|
|
||||||
<el-dropdown class="web-ssh" type="primary" trigger="click">
|
|
||||||
<!-- <el-button type="primary" @click="handleSSH">Web SSH</el-button> -->
|
|
||||||
<el-button type="primary">功能</el-button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item @click="handleSSH">连接终端</el-dropdown-item>
|
|
||||||
<el-dropdown-item v-if="consoleUrl" @click="handleToConsole">控制台</el-dropdown-item>
|
|
||||||
<el-dropdown-item @click="handleUpdate">修改服务器</el-dropdown-item>
|
|
||||||
<el-dropdown-item @click="handleRemoveHost"><span style="color: #727272;">移除主机</span></el-dropdown-item>
|
|
||||||
<el-dropdown-item @click="handleRemoveSSH"><span style="color: #727272;">移除凭证</span></el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SSHForm v-model:show="sshFormVisible" :temp-host="tempHost" :name="name" />
|
|
||||||
</el-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, getCurrentInstance } from 'vue'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import SSHForm from './ssh-form.vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $tools } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
hostInfo: {
|
|
||||||
required: true,
|
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
hiddenIp: {
|
|
||||||
required: true,
|
|
||||||
type: [Number, Boolean,]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update-list', 'update-host',])
|
|
||||||
|
|
||||||
const sshFormVisible = ref(false)
|
|
||||||
const tempHost = ref('')
|
|
||||||
|
|
||||||
const hostIp = computed(() => {
|
|
||||||
let ip = props.hostInfo?.ipInfo?.query || props.hostInfo?.host || '--'
|
|
||||||
try {
|
|
||||||
let formatIp = ip.replace(/\d/g, '*').split('.').map((item) => item.padStart(3, '*')).join('.')
|
|
||||||
return props.hiddenIp ? formatIp : ip
|
|
||||||
} catch (error) {
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const host = computed(() => props.hostInfo?.host)
|
|
||||||
const name = computed(() => props.hostInfo?.name)
|
|
||||||
const ping = computed(() => props.hostInfo?.ping || '')
|
|
||||||
const expiredTime = computed(() => $tools.formatTimestamp(props.hostInfo?.expired, 'date'))
|
|
||||||
const consoleUrl = computed(() => props.hostInfo?.consoleUrl)
|
|
||||||
const ipInfo = computed(() => props.hostInfo?.ipInfo || {})
|
|
||||||
const isError = computed(() => !Boolean(props.hostInfo?.osInfo))
|
|
||||||
const cpuInfo = computed(() => props.hostInfo?.cpuInfo || {})
|
|
||||||
const memInfo = computed(() => props.hostInfo?.memInfo || {})
|
|
||||||
const osInfo = computed(() => props.hostInfo?.osInfo || {})
|
|
||||||
const driveInfo = computed(() => props.hostInfo?.driveInfo || {})
|
|
||||||
const netstatInfo = computed(() => {
|
|
||||||
let { total: netTotal, ...netCards } = props.hostInfo?.netstatInfo || {}
|
|
||||||
return { netTotal, netCards: netCards || {} }
|
|
||||||
})
|
|
||||||
const openedCount = computed(() => props.hostInfo?.openedCount || 0)
|
|
||||||
|
|
||||||
const setColor = (num) => {
|
|
||||||
num = Number(num)
|
|
||||||
return num ? (num < 80 ? '#595959' : (num >= 80 && num < 90 ? '#FF6600' : '#FF0000')) : '#595959'
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdate = () => {
|
|
||||||
let { expired, expiredNotify, group, consoleUrl, remark } = props.hostInfo
|
|
||||||
emit('update-host', { name: name.value, host: host.value, expired, expiredNotify, group, consoleUrl, remark })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToConsole = () => {
|
|
||||||
window.open(consoleUrl.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSSH = async () => {
|
|
||||||
let { data } = await $api.existSSH(host.value)
|
|
||||||
if (data) return window.open(`/terminal?host=${ host.value }&name=${ name.value }`)
|
|
||||||
if (!host.value) {
|
|
||||||
return ElMessage({
|
|
||||||
message: '请等待获取服务器ip或刷新页面重试',
|
|
||||||
type: 'warning',
|
|
||||||
center: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
tempHost.value = host.value
|
|
||||||
sshFormVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveSSH = async () => {
|
|
||||||
ElMessageBox.confirm('确认删除SSH凭证', 'Warning', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
let { data } = await $api.removeSSH(host.value)
|
|
||||||
ElMessage({
|
|
||||||
message: data,
|
|
||||||
type: 'success',
|
|
||||||
center: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveHost = async () => {
|
|
||||||
ElMessageBox.confirm('确认删除主机', 'Warning', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
let { data } = await $api.removeHost({ host: host.value })
|
|
||||||
ElMessage({
|
|
||||||
message: data,
|
|
||||||
type: 'success',
|
|
||||||
center: true
|
|
||||||
})
|
|
||||||
emit('update-list')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.host-card {
|
|
||||||
margin: 0px 30px 20px;
|
|
||||||
transition: all 0.5s;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0px 0px 15px rgba(6, 30, 37, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-state {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 8px;
|
|
||||||
// transform: rotate(-45deg);
|
|
||||||
transform: scale(0.9);
|
|
||||||
display: inline-block;
|
|
||||||
padding: 3px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.online {
|
|
||||||
color: #009933;
|
|
||||||
background-color: #e8fff3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.offline {
|
|
||||||
color: #FF0033;
|
|
||||||
background-color: #fff5f8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 50px;
|
|
||||||
|
|
||||||
&>div {
|
|
||||||
flex: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.svg-icon {
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
color: #1989fa;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fields {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
// justify-content: center;
|
|
||||||
span {
|
|
||||||
padding: 3px 0;
|
|
||||||
margin-left: 5px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #595959;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
display: inline-block;
|
|
||||||
height: 19px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration-line: underline;
|
|
||||||
text-decoration-color: #1989fa;
|
|
||||||
|
|
||||||
.svg-icon {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-icon {
|
|
||||||
display: none;
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.web-ssh {
|
|
||||||
|
|
||||||
// ::v-deep has been deprecated. Use :deep(<inner-selector>) instead.
|
|
||||||
:deep(.el-dropdown__caret-button) {
|
|
||||||
margin-left: -5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.field-detail {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 0px 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
span {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #797979;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,208 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
width="400px"
|
|
||||||
:title="title"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
@open="setDefaultData"
|
|
||||||
@closed="handleClosed"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="hostForm"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="100px"
|
|
||||||
>
|
|
||||||
<transition-group
|
|
||||||
name="list"
|
|
||||||
mode="out-in"
|
|
||||||
tag="div"
|
|
||||||
>
|
|
||||||
<el-form-item key="group" label="分组" prop="group">
|
|
||||||
<el-select
|
|
||||||
v-model="hostForm.group"
|
|
||||||
placeholder="服务器分组"
|
|
||||||
style="width: 100%;"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in groupList"
|
|
||||||
:key="item.id"
|
|
||||||
:label="item.name"
|
|
||||||
:value="item.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item key="name" label="主机别名" prop="name">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="hostForm.name"
|
|
||||||
clearable
|
|
||||||
placeholder="主机别名"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item key="host" label="IP/域名" prop="host">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="hostForm.host"
|
|
||||||
clearable
|
|
||||||
placeholder="IP/域名"
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="handleSave"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item key="expired" label="到期时间" prop="expired">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="hostForm.expired"
|
|
||||||
type="date"
|
|
||||||
value-format="x"
|
|
||||||
placeholder="服务器到期时间"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item
|
|
||||||
v-if="hostForm.expired"
|
|
||||||
key="expiredNotify"
|
|
||||||
label="到期提醒"
|
|
||||||
prop="expiredNotify"
|
|
||||||
>
|
|
||||||
<el-tooltip content="将在服务器到期前7、3、1天发送提醒(需在设置中绑定有效邮箱)" placement="right">
|
|
||||||
<el-switch
|
|
||||||
v-model="hostForm.expiredNotify"
|
|
||||||
:active-value="true"
|
|
||||||
:inactive-value="false"
|
|
||||||
/>
|
|
||||||
</el-tooltip>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item key="consoleUrl" label="控制台URL" prop="consoleUrl">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="hostForm.consoleUrl"
|
|
||||||
clearable
|
|
||||||
placeholder="用于直达服务器控制台"
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="handleSave"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item key="remark" label="备注" prop="remark">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="hostForm.remark"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
clearable
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="用于简单记录服务器用途"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</transition-group>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="visible = false">关闭</el-button>
|
|
||||||
<el-button type="primary" @click="handleSave">确认</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, computed, watch, getCurrentInstance, nextTick } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
show: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
defaultData: {
|
|
||||||
required: false,
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const emit = defineEmits(['update:show', 'update-list', 'closed',])
|
|
||||||
|
|
||||||
const resetForm = () => ({
|
|
||||||
group: 'default',
|
|
||||||
name: '',
|
|
||||||
host: '',
|
|
||||||
expired: null,
|
|
||||||
expiredNotify: false,
|
|
||||||
consoleUrl: '',
|
|
||||||
remark: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const hostForm = reactive(resetForm())
|
|
||||||
const oldHost = ref('')
|
|
||||||
const groupList = ref([])
|
|
||||||
const rules = reactive({
|
|
||||||
group: { required: true, message: '选择一个分组' },
|
|
||||||
name: { required: true, message: '输入主机别名', trigger: 'change' },
|
|
||||||
host: { required: true, message: '输入IP/域名', trigger: 'change' },
|
|
||||||
expired: { required: false },
|
|
||||||
expiredNotify: { required: false },
|
|
||||||
consoleUrl: { required: false },
|
|
||||||
remark: { required: false }
|
|
||||||
})
|
|
||||||
|
|
||||||
const formRef = ref(null)
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.show,
|
|
||||||
set: (newVal) => emit('update:show', newVal)
|
|
||||||
})
|
|
||||||
|
|
||||||
const title = computed(() => props.defaultData ? '修改服务器' : '新增服务器')
|
|
||||||
|
|
||||||
watch(() => props.show, (newVal) => {
|
|
||||||
if (!newVal) return
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
|
|
||||||
const getGroupList = () => {
|
|
||||||
$api.getGroupList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
groupList.value = data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClosed = () => {
|
|
||||||
// console.log('handleClosed')
|
|
||||||
Object.assign(hostForm, resetForm())
|
|
||||||
emit('closed')
|
|
||||||
nextTick(() => formRef.value.resetFields())
|
|
||||||
}
|
|
||||||
|
|
||||||
const setDefaultData = () => {
|
|
||||||
if (!props.defaultData) return
|
|
||||||
let { name, host, expired, expiredNotify, consoleUrl, group, remark } = props.defaultData
|
|
||||||
oldHost.value = host
|
|
||||||
Object.assign(hostForm, { name, host, expired, expiredNotify, consoleUrl, group, remark })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
formRef.value.validate()
|
|
||||||
.then(async () => {
|
|
||||||
if (!hostForm.expired || !hostForm.expiredNotify) {
|
|
||||||
hostForm.expired = null
|
|
||||||
hostForm.expiredNotify = false
|
|
||||||
}
|
|
||||||
if (props.defaultData) {
|
|
||||||
let { msg } = await $api.updateHost(Object.assign({}, hostForm, { oldHost: oldHost.value }))
|
|
||||||
$message({ type: 'success', center: true, message: msg })
|
|
||||||
} else {
|
|
||||||
let { msg } = await $api.saveHost(hostForm)
|
|
||||||
$message({ type: 'success', center: true, message: msg })
|
|
||||||
}
|
|
||||||
visible.value = false
|
|
||||||
emit('update-list')
|
|
||||||
Object.assign(hostForm, resetForm())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,185 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-loading="loading">
|
|
||||||
<el-form
|
|
||||||
ref="emailFormRef"
|
|
||||||
:model="emailForm"
|
|
||||||
:rules="rules"
|
|
||||||
:inline="true"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
>
|
|
||||||
<el-form-item label="" prop="target" style="width: 200px;">
|
|
||||||
<el-select
|
|
||||||
v-model="emailForm.target"
|
|
||||||
placeholder="邮件服务商"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in supportEmailList"
|
|
||||||
:key="item.target"
|
|
||||||
:label="item.name"
|
|
||||||
:value="item.target"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="" prop="auth.user" style="width: 200px;">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="emailForm.auth.user"
|
|
||||||
clearable
|
|
||||||
placeholder="邮箱"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="" prop="auth.pass" style="width: 200px;">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="emailForm.auth.pass"
|
|
||||||
clearable
|
|
||||||
placeholder="SMTP授权码"
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="addEmail"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="">
|
|
||||||
<el-tooltip
|
|
||||||
effect="dark"
|
|
||||||
content="重复添加的邮箱将会被覆盖"
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<el-button type="primary" @click="addEmail">
|
|
||||||
添加
|
|
||||||
</el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<!-- 提示 -->
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;"> Tips: 系统所有通知邮件将会下发到所有已经配置成功的邮箱中 </span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
<!-- 表格 -->
|
|
||||||
<el-table :data="userEmailList" class="table">
|
|
||||||
<el-table-column prop="email" label="Email" />
|
|
||||||
<el-table-column prop="name" label="服务商" />
|
|
||||||
<el-table-column label="操作">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:loading="row.loading"
|
|
||||||
@click="pushTestEmail(row)"
|
|
||||||
>
|
|
||||||
测试
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
@click="deleteUserEmail(row)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $messageBox, $notification } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const userEmailList = ref([])
|
|
||||||
const supportEmailList = ref([])
|
|
||||||
const emailFormRef = ref(null)
|
|
||||||
|
|
||||||
const emailForm = reactive({
|
|
||||||
target: 'qq',
|
|
||||||
auth: {
|
|
||||||
user: '',
|
|
||||||
pass: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const rules = reactive({
|
|
||||||
'auth.user': { required: true, type: 'email', message: '需输入邮箱', trigger: 'change' },
|
|
||||||
'auth.pass': { required: true, message: '需输入SMTP授权码', trigger: 'change' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const getUserEmailList = () => {
|
|
||||||
loading.value = true
|
|
||||||
$api.getUserEmailList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
userEmailList.value = data.map(item => {
|
|
||||||
item.loading = false
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => loading.value = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSupportEmailList = () => {
|
|
||||||
$api.getSupportEmailList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
supportEmailList.value = data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const addEmail = () => {
|
|
||||||
emailFormRef.value.validate()
|
|
||||||
.then(() => {
|
|
||||||
$api.updateUserEmailList({ ...emailForm })
|
|
||||||
.then(() => {
|
|
||||||
$message.success('添加成功, 点击[测试]按钮发送测试邮件')
|
|
||||||
let { target } = emailForm
|
|
||||||
emailForm.target = target
|
|
||||||
emailForm.auth.user = ''
|
|
||||||
emailForm.auth.pass = ''
|
|
||||||
getUserEmailList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const pushTestEmail = (row) => {
|
|
||||||
row.loading = true
|
|
||||||
const { email: toEmail } = row
|
|
||||||
$api.pushTestEmail({ isTest: true, toEmail })
|
|
||||||
.then(() => {
|
|
||||||
$message.success(`发送成功, 请检查邮箱: ${ toEmail }`)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
$notification({
|
|
||||||
title: '发送测试邮件失败, 请检查邮箱SMTP配置',
|
|
||||||
message: error.response?.data.msg,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
row.loading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteUserEmail = ({ email }) => {
|
|
||||||
$messageBox.confirm(
|
|
||||||
`确认删除邮箱:${ email }`,
|
|
||||||
'Warning',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(async () => {
|
|
||||||
await $api.deleteUserEmail(email)
|
|
||||||
$message.success('success')
|
|
||||||
getUserEmailList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getUserEmailList()
|
|
||||||
getSupportEmailList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,239 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-form
|
|
||||||
ref="groupFormRef"
|
|
||||||
:model="groupForm"
|
|
||||||
:rules="rules"
|
|
||||||
:inline="true"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
>
|
|
||||||
<el-form-item label="" prop="name" style="width: 200px;">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="groupForm.name"
|
|
||||||
clearable
|
|
||||||
placeholder="分组名称"
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="addGroup"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="" prop="index" style="width: 200px;">
|
|
||||||
<el-input
|
|
||||||
v-model.number="groupForm.index"
|
|
||||||
clearable
|
|
||||||
placeholder="序号(数字, 用于分组排序)"
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="addGroup"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="">
|
|
||||||
<el-button type="primary" @click="addGroup">
|
|
||||||
添加
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<!-- 提示 -->
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;">
|
|
||||||
Tips: 已添加服务器数量 <u>{{ hostGroupInfo.total }}</u>
|
|
||||||
<span v-show="hostGroupInfo.notGroupCount">, 有 <u>{{ hostGroupInfo.notGroupCount }}</u> 台服务器尚未分组</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-alert><br>
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;"> Tips: 删除分组会将分组内所有服务器移至默认分组 </span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
<el-table v-loading="loading" :data="list">
|
|
||||||
<el-table-column prop="index" label="序号" />
|
|
||||||
<el-table-column prop="id" label="ID" />
|
|
||||||
<el-table-column prop="name" label="分组名称" />
|
|
||||||
<el-table-column label="关联服务器数量">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-popover
|
|
||||||
v-if="row.hosts.list.length !== 0"
|
|
||||||
placement="right"
|
|
||||||
:width="350"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #reference>
|
|
||||||
<u class="host-count">{{ row.hosts.count }}</u>
|
|
||||||
</template>
|
|
||||||
<ul>
|
|
||||||
<li v-for="item in row.hosts.list" :key="item.host">
|
|
||||||
<span>{{ item.host }}</span>
|
|
||||||
-
|
|
||||||
<span>{{ item.name }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</el-popover>
|
|
||||||
<u v-else class="host-count">0</u>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button type="primary" @click="handleChange(row)">修改</el-button>
|
|
||||||
<el-button v-show="row.id !== 'default'" type="danger" @click="deleteGroup(row)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
width="400px"
|
|
||||||
title="修改分组"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="updateFormRef"
|
|
||||||
:model="updateForm"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="100px"
|
|
||||||
>
|
|
||||||
<el-form-item label="分组名称" prop="name">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="updateForm.name"
|
|
||||||
clearable
|
|
||||||
placeholder="分组名称"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="分组序号" prop="index">
|
|
||||||
<el-input
|
|
||||||
v-model.number="updateForm.index"
|
|
||||||
clearable
|
|
||||||
placeholder="分组序号"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="visible = false">关闭</el-button>
|
|
||||||
<el-button type="primary" @click="updateGroup">修改</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const visible = ref(false)
|
|
||||||
const groupList = ref([])
|
|
||||||
const groupForm = reactive({
|
|
||||||
name: '',
|
|
||||||
index: ''
|
|
||||||
})
|
|
||||||
const updateForm = reactive({
|
|
||||||
name: '',
|
|
||||||
index: ''
|
|
||||||
})
|
|
||||||
const rules = reactive({
|
|
||||||
name: { required: true, message: '需输入分组名称', trigger: 'change' },
|
|
||||||
index: { required: true, type: 'number', message: '需输入数字', trigger: 'change' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const groupFormRef = ref(null)
|
|
||||||
const updateFormRef = ref(null)
|
|
||||||
|
|
||||||
const hostGroupInfo = computed(() => {
|
|
||||||
const total = $store.hostList.length
|
|
||||||
const notGroupCount = $store.hostList.reduce((prev, next) => {
|
|
||||||
if (!next.group) prev++
|
|
||||||
return prev
|
|
||||||
}, 0)
|
|
||||||
return { total, notGroupCount }
|
|
||||||
})
|
|
||||||
|
|
||||||
const list = computed(() => {
|
|
||||||
return groupList.value.map(item => {
|
|
||||||
const hosts = $store.hostList.reduce((prev, next) => {
|
|
||||||
if (next.group === item.id) {
|
|
||||||
prev.count++
|
|
||||||
prev.list.push(next)
|
|
||||||
}
|
|
||||||
return prev
|
|
||||||
}, { count: 0, list: [] })
|
|
||||||
return { ...item, hosts }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const getGroupList = () => {
|
|
||||||
loading.value = true
|
|
||||||
$api.getGroupList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
groupList.value = data
|
|
||||||
groupForm.index = data.length
|
|
||||||
})
|
|
||||||
.finally(() => loading.value = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addGroup = () => {
|
|
||||||
groupFormRef.value.validate()
|
|
||||||
.then(() => {
|
|
||||||
const { name, index } = groupForm
|
|
||||||
$api.addGroup({ name, index })
|
|
||||||
.then(() => {
|
|
||||||
$message.success('success')
|
|
||||||
groupForm.name = ''
|
|
||||||
groupForm.index = ''
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = ({ id, name, index }) => {
|
|
||||||
updateForm.id = id
|
|
||||||
updateForm.name = name
|
|
||||||
updateForm.index = index
|
|
||||||
visible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGroup = () => {
|
|
||||||
updateFormRef.value.validate()
|
|
||||||
.then(() => {
|
|
||||||
const { id, name, index } = updateForm
|
|
||||||
$api.updateGroup(id, { name, index })
|
|
||||||
.then(() => {
|
|
||||||
$message.success('success')
|
|
||||||
visible.value = false
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteGroup = ({ id, name }) => {
|
|
||||||
$messageBox.confirm(`确认删除分组:${ name }`, 'Warning', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await $api.deleteGroup(id)
|
|
||||||
await $store.getHostList()
|
|
||||||
$message.success('success')
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.host-count {
|
|
||||||
display: block;
|
|
||||||
width: 100px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #87cf63;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;"> Tips: 请添加邮箱并确保测试邮件通过 </span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
<el-table v-loading="notifyListLoading" :data="notifyList">
|
|
||||||
<el-table-column prop="desc" label="通知类型" />
|
|
||||||
<el-table-column prop="sw" label="开关">
|
|
||||||
<template #default="{row}">
|
|
||||||
<el-switch
|
|
||||||
v-model="row.sw"
|
|
||||||
:active-value="true"
|
|
||||||
:inactive-value="false"
|
|
||||||
:loading="row.loading"
|
|
||||||
@change="handleChangeSw(row, $event)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const notifyListLoading = ref(false)
|
|
||||||
const notifyList = ref([])
|
|
||||||
|
|
||||||
const getNotifyList = (flag = true) => {
|
|
||||||
if (flag) notifyListLoading.value = true
|
|
||||||
$api.getNotifyList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
notifyList.value = data.map((item) => {
|
|
||||||
item.loading = false
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => notifyListLoading.value = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeSw = async (row) => {
|
|
||||||
row.loading = true
|
|
||||||
const { type, sw } = row
|
|
||||||
try {
|
|
||||||
await $api.updateNotifyList({ type, sw })
|
|
||||||
// if (this.userEmailList.length === 0) $message.warning('未配置邮箱, 此开关将不会生效')
|
|
||||||
} finally {
|
|
||||||
row.loading = false
|
|
||||||
}
|
|
||||||
getNotifyList(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getNotifyList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,98 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
class="password-form"
|
|
||||||
:model="formData"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="90px"
|
|
||||||
:show-message="false"
|
|
||||||
>
|
|
||||||
<el-form-item label="原用户名" prop="oldLoginName">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.oldLoginName"
|
|
||||||
clearable
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="原密码" prop="oldPwd">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.oldPwd"
|
|
||||||
type="password"
|
|
||||||
clearable
|
|
||||||
show-password
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="新用户名" prop="oldPwd">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.newLoginName"
|
|
||||||
clearable
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="新密码" prop="newPwd">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.newPwd"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="handleUpdate"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" :loading="loading" @click="handleUpdate">确认</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, getCurrentInstance } from 'vue'
|
|
||||||
import { RSAEncrypt } from '@utils/index.js'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const formRef = ref(null)
|
|
||||||
const formData = reactive({
|
|
||||||
oldLoginName: '',
|
|
||||||
oldPwd: '',
|
|
||||||
newLoginName: '',
|
|
||||||
newPwd: ''
|
|
||||||
})
|
|
||||||
const rules = reactive({
|
|
||||||
oldLoginName: { required: true, message: '输入原用户名', trigger: 'change' },
|
|
||||||
oldPwd: { required: true, message: '输入原密码', trigger: 'change' },
|
|
||||||
newLoginName: { required: true, message: '输入新用户名', trigger: 'change' },
|
|
||||||
newPwd: { required: true, message: '输入新密码', trigger: 'change' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleUpdate = () => {
|
|
||||||
formRef.value.validate()
|
|
||||||
.then(async () => {
|
|
||||||
let { oldLoginName, oldPwd, newLoginName, newPwd } = formData
|
|
||||||
oldPwd = RSAEncrypt(oldPwd)
|
|
||||||
newPwd = RSAEncrypt(newPwd)
|
|
||||||
let { msg } = await $api.updatePwd({ oldLoginName, oldPwd, newLoginName, newPwd })
|
|
||||||
$message({ type: 'success', center: true, message: msg })
|
|
||||||
$store.setUser(newLoginName)
|
|
||||||
formData.oldLoginName = ''
|
|
||||||
formData.oldPwd = ''
|
|
||||||
formData.newLoginName = ''
|
|
||||||
formData.newPwd = ''
|
|
||||||
formRef.value.resetFields()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.password-form {
|
|
||||||
width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,46 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;"> Tips: 系统只保存最近10条登录记录, 检测到更换IP后需重新登录 </span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
<el-table v-loading="loading" :data="loginRecordList">
|
|
||||||
<el-table-column prop="ip" label="IP" />
|
|
||||||
<el-table-column prop="address" label="地点" show-overflow-tooltip>
|
|
||||||
<template #default="scope">
|
|
||||||
<span style="letter-spacing: 2px;"> {{ scope.row.country }} {{ scope.row.city }} </span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="date" label="时间" />
|
|
||||||
</el-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $tools } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const loginRecordList = ref([])
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const handleLookupLoginRecord = () => {
|
|
||||||
loading.value = true
|
|
||||||
$api.getLoginRecord()
|
|
||||||
.then(({ data }) => {
|
|
||||||
loginRecordList.value = data.map((item) => {
|
|
||||||
item.date = $tools.formatTimestamp(item.date)
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
handleLookupLoginRecord()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
</style>
|
|
@ -1,98 +0,0 @@
|
|||||||
<template>
|
|
||||||
<transition-group
|
|
||||||
name="list"
|
|
||||||
mode="out-in"
|
|
||||||
tag="ul"
|
|
||||||
class="host-list"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(item, index) in list"
|
|
||||||
:key="item.host"
|
|
||||||
:draggable="true"
|
|
||||||
class="host-item"
|
|
||||||
@dragenter="dragenter($event, index)"
|
|
||||||
@dragover="dragover($event)"
|
|
||||||
@dragstart="dragstart(index)"
|
|
||||||
>
|
|
||||||
<span>{{ item.host }}</span>
|
|
||||||
---
|
|
||||||
<span>{{ item.name }}</span>
|
|
||||||
</li>
|
|
||||||
</transition-group>
|
|
||||||
<div style="display: flex; justify-content: center; margin-top: 25px">
|
|
||||||
<el-button type="primary" @click="handleUpdateSort">
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const emit = defineEmits(['update-list',])
|
|
||||||
const { proxy: { $api, $message, $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const targetIndex = ref(0)
|
|
||||||
const list = ref([])
|
|
||||||
|
|
||||||
const dragstart = (index) => {
|
|
||||||
targetIndex.value = index
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragenter = (e, curIndex) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (targetIndex.value !== curIndex) {
|
|
||||||
let target = list.value.splice(targetIndex.value, 1)[0]
|
|
||||||
list.value.splice(curIndex, 0, target)
|
|
||||||
targetIndex.value = curIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragover = (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateSort = () => {
|
|
||||||
$api.updateHostSort({ list: list.value })
|
|
||||||
.then(({ msg }) => {
|
|
||||||
$message({ type: 'success', center: true, message: msg })
|
|
||||||
$store.sortHostList(list.value)
|
|
||||||
emit('update-list', list.value) // 触发自定义事件
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
list.value = $store.hostList.map(({ name, host }) => ({ name, host }))
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.drag-move {
|
|
||||||
transition: transform .3s;
|
|
||||||
}
|
|
||||||
.host-list {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-right: 50px;
|
|
||||||
}
|
|
||||||
.host-item {
|
|
||||||
transition: all .3s;
|
|
||||||
box-shadow: var(--el-box-shadow-lighter);
|
|
||||||
cursor: move;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #595959;
|
|
||||||
padding: 0 20px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #000;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
height: 35px;
|
|
||||||
line-height: 35px;
|
|
||||||
&:hover {
|
|
||||||
box-shadow: var(--el-box-shadow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,69 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
width="1100px"
|
|
||||||
:title="'功能设置'"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
:close-on-press-escape="false"
|
|
||||||
>
|
|
||||||
<el-tabs style="height: 500px;" tab-position="left">
|
|
||||||
<el-tab-pane label="分组管理">
|
|
||||||
<Group />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="登录记录">
|
|
||||||
<Record />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="主机排序" lazy>
|
|
||||||
<Sort @update-list="emitUpdateList" />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="全局通知" lazy>
|
|
||||||
<NotifyList />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="邮箱配置" lazy>
|
|
||||||
<EmailList />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="修改密码" lazy>
|
|
||||||
<Password />
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import NotifyList from './setting-tab/notify-list.vue'
|
|
||||||
import EmailList from './setting-tab/email-list.vue'
|
|
||||||
import Sort from './setting-tab/sort.vue'
|
|
||||||
import Record from './setting-tab/record.vue'
|
|
||||||
import Group from './setting-tab/group.vue'
|
|
||||||
import Password from './setting-tab/password.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
show: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:show', 'update-list',])
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.show,
|
|
||||||
set: (newVal) => emit('update:show', newVal)
|
|
||||||
})
|
|
||||||
|
|
||||||
const emitUpdateList = () => {
|
|
||||||
emit('update-list')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.table {
|
|
||||||
max-height: 400px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,205 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
title="SSH连接"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
@closed="clearFormInfo"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="sshForm"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="90px"
|
|
||||||
>
|
|
||||||
<el-form-item label="主机" prop="host">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="sshForm.host"
|
|
||||||
disabled
|
|
||||||
clearable
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="端口" prop="port">
|
|
||||||
<el-input v-model.trim="sshForm.port" clearable autocomplete="off" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="用户名" prop="username">
|
|
||||||
<el-autocomplete
|
|
||||||
v-model.trim="sshForm.username"
|
|
||||||
:fetch-suggestions="userSearch"
|
|
||||||
style="width: 100%;"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #default="{item}">
|
|
||||||
<div class="value">{{ item.value }}</div>
|
|
||||||
</template>
|
|
||||||
</el-autocomplete>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="认证方式" prop="type">
|
|
||||||
<el-radio v-model.trim="sshForm.type" value="privateKey">密钥</el-radio>
|
|
||||||
<el-radio v-model.trim="sshForm.type" value="password">密码</el-radio>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="sshForm.type === 'password'" prop="password" label="密码">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="sshForm.password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Please input password"
|
|
||||||
autocomplete="off"
|
|
||||||
clearable
|
|
||||||
show-password
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="sshForm.type === 'privateKey'" prop="privateKey" label="密钥">
|
|
||||||
<el-button type="primary" size="small" @click="handleClickUploadBtn">
|
|
||||||
选择私钥...
|
|
||||||
</el-button>
|
|
||||||
<input
|
|
||||||
ref="privateKeyRef"
|
|
||||||
type="file"
|
|
||||||
name="privateKey"
|
|
||||||
style="display: none;"
|
|
||||||
@change="handleSelectPrivateKeyFile"
|
|
||||||
>
|
|
||||||
<el-input
|
|
||||||
v-model.trim="sshForm.privateKey"
|
|
||||||
type="textarea"
|
|
||||||
:rows="5"
|
|
||||||
clearable
|
|
||||||
autocomplete="off"
|
|
||||||
style="margin-top: 5px;"
|
|
||||||
placeholder="-----BEGIN RSA PRIVATE KEY-----"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="command" label="执行指令">
|
|
||||||
<el-input
|
|
||||||
v-model="sshForm.command"
|
|
||||||
type="textarea"
|
|
||||||
:rows="5"
|
|
||||||
clearable
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="连接服务器后自动执行的指令(例如: sudo -i)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="visible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSaveSSH">保存</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, computed, watch, getCurrentInstance, nextTick } from 'vue'
|
|
||||||
import { ElNotification } from 'element-plus'
|
|
||||||
import { randomStr, AESEncrypt, RSAEncrypt } from '@utils/index.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
show: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
tempHost: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:show',])
|
|
||||||
|
|
||||||
const formRef = ref(null)
|
|
||||||
const privateKeyRef = ref(null)
|
|
||||||
const sshForm = reactive({
|
|
||||||
host: '',
|
|
||||||
port: 22,
|
|
||||||
username: '',
|
|
||||||
type: 'privateKey',
|
|
||||||
password: '',
|
|
||||||
privateKey: '',
|
|
||||||
command: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const defaultUsers = [
|
|
||||||
{ value: 'root' },
|
|
||||||
{ value: 'ubuntu' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const rules = reactive({
|
|
||||||
host: { required: true, message: '需输入主机', trigger: 'change' },
|
|
||||||
port: { required: true, message: '需输入端口', trigger: 'change' },
|
|
||||||
username: { required: true, message: '需输入用户名', trigger: 'change' },
|
|
||||||
type: { required: true },
|
|
||||||
password: { required: true, message: '需输入密码', trigger: 'change' },
|
|
||||||
privateKey: { required: true, message: '需输入密钥', trigger: 'change' },
|
|
||||||
command: { required: false }
|
|
||||||
})
|
|
||||||
|
|
||||||
const { proxy: { $api } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get() {
|
|
||||||
return props.show
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
emit('update:show', newVal)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.tempHost, (newVal) => {
|
|
||||||
sshForm.host = newVal
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleClickUploadBtn = () => {
|
|
||||||
privateKeyRef.value.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectPrivateKeyFile = (event) => {
|
|
||||||
let file = event.target.files[0]
|
|
||||||
let reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
sshForm.privateKey = e.target.result
|
|
||||||
privateKeyRef.value.value = ''
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveSSH = () => {
|
|
||||||
formRef.value.validate()
|
|
||||||
.then(async () => {
|
|
||||||
let randomKey = randomStr(16)
|
|
||||||
let formData = JSON.parse(JSON.stringify(sshForm))
|
|
||||||
// 加密传输
|
|
||||||
if (formData.password) formData.password = AESEncrypt(formData.password, randomKey)
|
|
||||||
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, randomKey)
|
|
||||||
formData.randomKey = RSAEncrypt(randomKey)
|
|
||||||
await $api.updateSSH(formData)
|
|
||||||
ElNotification({
|
|
||||||
title: '保存成功',
|
|
||||||
message: '下次点击 [Web SSH] 可直接登录终端\n如无法登录请 [移除凭证] 后重新添加',
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
visible.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const userSearch = (keyword, cb) => {
|
|
||||||
let res = keyword
|
|
||||||
? defaultUsers.filter((item) => item.value.includes(keyword))
|
|
||||||
: defaultUsers
|
|
||||||
cb(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearFormInfo = () => {
|
|
||||||
nextTick(() => formRef.value.resetFields())
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,185 +0,0 @@
|
|||||||
<template>
|
|
||||||
<header>
|
|
||||||
<div class="logo-wrap">
|
|
||||||
<img src="@/assets/logo.png" alt="logo">
|
|
||||||
<h1>EasyNode</h1>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button type="primary" @click="hostFormVisible = true">
|
|
||||||
新增服务器
|
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" @click="settingVisible = true">
|
|
||||||
功能设置
|
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" @click="handleHiddenIP">
|
|
||||||
{{ hiddenIp ? '显示IP' : '隐藏IP' }}
|
|
||||||
</el-button>
|
|
||||||
<el-button type="success" plain @click="handleLogout">安全退出</el-button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<section
|
|
||||||
v-loading="loading"
|
|
||||||
element-loading-background="rgba(122, 122, 122, 0.58)"
|
|
||||||
>
|
|
||||||
<HostCard
|
|
||||||
v-for="(item, index) in hostListStatus"
|
|
||||||
:key="index"
|
|
||||||
:host-info="item"
|
|
||||||
:hidden-ip="hiddenIp"
|
|
||||||
@update-list="handleUpdateList"
|
|
||||||
@update-host="handleUpdateHost"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<footer>
|
|
||||||
<span>Release v1.2.1, Powered by <a href="https://github.com/chaos-zhu/easynode" target="_blank">EasyNode</a></span>
|
|
||||||
</footer>
|
|
||||||
<HostForm
|
|
||||||
v-model:show="hostFormVisible"
|
|
||||||
:default-data="updateHostData"
|
|
||||||
@update-list="handleUpdateList"
|
|
||||||
@closed="updateHostData = null"
|
|
||||||
/>
|
|
||||||
<Setting
|
|
||||||
v-model:show="settingVisible"
|
|
||||||
@update-list="handleUpdateList"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
|
|
||||||
import { io } from 'socket.io-client'
|
|
||||||
import HostForm from './components/host-form.vue'
|
|
||||||
import Setting from './components/setting.vue'
|
|
||||||
import HostCard from './components/host-card.vue'
|
|
||||||
|
|
||||||
const { proxy: { $store, $message, $notification, $router, $serviceURI } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const socket = ref(null)
|
|
||||||
const loading = ref(true)
|
|
||||||
const hostListStatus = ref([])
|
|
||||||
const updateHostData = ref(null)
|
|
||||||
const hostFormVisible = ref(false)
|
|
||||||
const settingVisible = ref(false)
|
|
||||||
const hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
$store.clearJwtToken()
|
|
||||||
$message({ type: 'success', message: '已安全退出', center: true })
|
|
||||||
$router.push('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getHostList = async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
await $store.getHostList()
|
|
||||||
connectIo()
|
|
||||||
} catch (err) {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectIo = () => {
|
|
||||||
let socketInstance = io($serviceURI, {
|
|
||||||
path: '/clients',
|
|
||||||
forceNew: true,
|
|
||||||
reconnectionDelay: 5000,
|
|
||||||
reconnectionAttempts: 2
|
|
||||||
})
|
|
||||||
socket.value = socketInstance
|
|
||||||
socketInstance.on('connect', () => {
|
|
||||||
let flag = 5
|
|
||||||
loading.value = false
|
|
||||||
console.log('clients websocket 已连接: ', socketInstance.id)
|
|
||||||
let token = $store.token
|
|
||||||
socketInstance.emit('init_clients_data', { token })
|
|
||||||
socketInstance.on('clients_data', (data) => {
|
|
||||||
if ((flag++ % 5) === 0) $store.getHostPing()
|
|
||||||
hostListStatus.value = $store.hostList.map(item => {
|
|
||||||
const { host } = item
|
|
||||||
if (data[host] === null) return { ...item }
|
|
||||||
return Object.assign({}, item, data[host])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
socketInstance.on('token_verify_fail', (message) => {
|
|
||||||
$notification({
|
|
||||||
title: '鉴权失败',
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
$router.push('/login')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
socketInstance.on('disconnect', () => {
|
|
||||||
console.error('clients websocket 连接断开')
|
|
||||||
})
|
|
||||||
socketInstance.on('connect_error', (message) => {
|
|
||||||
loading.value = false
|
|
||||||
console.error('clients websocket 连接出错: ', message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateList = () => {
|
|
||||||
if (socket.value) socket.value.close()
|
|
||||||
getHostList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateHost = (defaultData) => {
|
|
||||||
hostFormVisible.value = true
|
|
||||||
updateHostData.value = defaultData
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleHiddenIP = () => {
|
|
||||||
hiddenIp.value = hiddenIp.value ? 0 : 1
|
|
||||||
localStorage.setItem('hiddenIp', String(hiddenIp.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getHostList()
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (socket.value) socket.value.close()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
$height:70px;
|
|
||||||
header {
|
|
||||||
padding: 0 30px;
|
|
||||||
height: $height;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
.logo-wrap {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
img {
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: white;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
opacity: 0.9;
|
|
||||||
height: calc(100vh - $height - 25px);
|
|
||||||
padding: 10px 0 250px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
height: 25px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
span {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #48ff00;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -160,7 +160,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SSHForm v-model:show="sshFormVisible" :temp-host="tempHost" :name="name" />
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -185,11 +184,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update-list', 'update-host',])
|
const emit = defineEmits(['update-list', 'update-host',])
|
||||||
|
|
||||||
const sshFormVisible = ref(false)
|
|
||||||
const tempHost = ref('')
|
|
||||||
|
|
||||||
const hostIp = computed(() => {
|
const hostIp = computed(() => {
|
||||||
let ip = props.hostInfo?.ipInfo?.query || props.hostInfo?.host || '--'
|
let ip = hostInfo.value?.ipInfo?.query || hostInfo.value?.host || '--'
|
||||||
try {
|
try {
|
||||||
let formatIp = ip.replace(/\d/g, '*').split('.').map((item) => item.padStart(3, '*')).join('.')
|
let formatIp = ip.replace(/\d/g, '*').split('.').map((item) => item.padStart(3, '*')).join('.')
|
||||||
return props.hiddenIp ? formatIp : ip
|
return props.hiddenIp ? formatIp : ip
|
||||||
@ -198,22 +194,23 @@ const hostIp = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const host = computed(() => props.hostInfo?.host)
|
const hostInfo = computed(() => props.hostInfo || {})
|
||||||
const name = computed(() => props.hostInfo?.name)
|
const host = computed(() => hostInfo.value?.host)
|
||||||
const ping = computed(() => props.hostInfo?.ping || '')
|
const name = computed(() => hostInfo.value?.name)
|
||||||
const expiredTime = computed(() => $tools.formatTimestamp(props.hostInfo?.expired, 'date'))
|
const ping = computed(() => hostInfo.value?.ping || '')
|
||||||
const consoleUrl = computed(() => props.hostInfo?.consoleUrl)
|
const expiredTime = computed(() => $tools.formatTimestamp(hostInfo.value?.expired, 'date'))
|
||||||
const ipInfo = computed(() => props.hostInfo?.ipInfo || {})
|
const consoleUrl = computed(() => hostInfo.value?.consoleUrl)
|
||||||
const isError = computed(() => !Boolean(props.hostInfo?.osInfo))
|
const ipInfo = computed(() => hostInfo.value?.ipInfo || {})
|
||||||
const cpuInfo = computed(() => props.hostInfo?.cpuInfo || {})
|
const isError = computed(() => !Boolean(hostInfo.value?.osInfo))
|
||||||
const memInfo = computed(() => props.hostInfo?.memInfo || {})
|
const cpuInfo = computed(() => hostInfo.value?.cpuInfo || {})
|
||||||
const osInfo = computed(() => props.hostInfo?.osInfo || {})
|
const memInfo = computed(() => hostInfo.value?.memInfo || {})
|
||||||
const driveInfo = computed(() => props.hostInfo?.driveInfo || {})
|
const osInfo = computed(() => hostInfo.value?.osInfo || {})
|
||||||
|
const driveInfo = computed(() => hostInfo.value?.driveInfo || {})
|
||||||
const netstatInfo = computed(() => {
|
const netstatInfo = computed(() => {
|
||||||
let { total: netTotal, ...netCards } = props.hostInfo?.netstatInfo || {}
|
let { total: netTotal, ...netCards } = hostInfo.value?.netstatInfo || {}
|
||||||
return { netTotal, netCards: netCards || {} }
|
return { netTotal, netCards: netCards || {} }
|
||||||
})
|
})
|
||||||
const openedCount = computed(() => props.hostInfo?.openedCount || 0)
|
const openedCount = computed(() => hostInfo.value?.openedCount || 0)
|
||||||
|
|
||||||
const setColor = (num) => {
|
const setColor = (num) => {
|
||||||
num = Number(num)
|
num = Number(num)
|
||||||
@ -221,8 +218,7 @@ const setColor = (num) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
let { expired, expiredNotify, group, consoleUrl, remark, index } = props.hostInfo
|
emit('update-host', hostInfo.value)
|
||||||
emit('update-host', { name: name.value, host: host.value, index, expired, expiredNotify, group, consoleUrl, remark })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToConsole = () => {
|
const handleToConsole = () => {
|
||||||
@ -230,17 +226,7 @@ const handleToConsole = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSSH = async () => {
|
const handleSSH = async () => {
|
||||||
let { data } = await $api.existSSH(host.value)
|
let { data } = host.value
|
||||||
if (data) return window.open(`/terminal?host=${ host.value }&name=${ name.value }`)
|
|
||||||
if (!host.value) {
|
|
||||||
return ElMessage({
|
|
||||||
message: '请等待获取服务器ip或刷新页面重试',
|
|
||||||
type: 'warning',
|
|
||||||
center: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
tempHost.value = host.value
|
|
||||||
sshFormVisible.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveSSH = async () => {
|
const handleRemoveSSH = async () => {
|
||||||
@ -259,7 +245,7 @@ const handleRemoveSSH = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveHost = async () => {
|
const handleRemoveHost = async () => {
|
||||||
ElMessageBox.confirm('确认删除主机', 'Warning', {
|
ElMessageBox.confirm('确认删除实例', 'Warning', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
width="400px"
|
width="600px"
|
||||||
:title="title"
|
:title="title"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@open="setDefaultData"
|
@open="setDefaultData"
|
||||||
@ -24,7 +24,7 @@
|
|||||||
<el-form-item key="group" label="分组" prop="group">
|
<el-form-item key="group" label="分组" prop="group">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="hostForm.group"
|
v-model="hostForm.group"
|
||||||
placeholder="服务器分组"
|
placeholder="实例分组"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@ -35,7 +35,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="name" label="服务器名称" prop="name">
|
<el-form-item key="name" label="名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="hostForm.name"
|
v-model.trim="hostForm.name"
|
||||||
clearable
|
clearable
|
||||||
@ -43,12 +43,102 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="host" label="服务器IP" prop="host">
|
<div key="instance_info" class="instance_info">
|
||||||
<el-input
|
<el-form-item
|
||||||
v-model.trim="hostForm.host"
|
key="host"
|
||||||
|
class="form_item"
|
||||||
|
label="实例"
|
||||||
|
prop="host"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model.trim="hostForm.host"
|
||||||
|
clearable
|
||||||
|
placeholder="IP"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
key="port"
|
||||||
|
class="form_item"
|
||||||
|
label="端口"
|
||||||
|
prop="port"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model.trim.number="hostForm.port"
|
||||||
|
clearable
|
||||||
|
placeholder="port"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item key="username" label="用户名" prop="username">
|
||||||
|
<el-autocomplete
|
||||||
|
v-model.trim="hostForm.username"
|
||||||
|
:fetch-suggestions="userSearch"
|
||||||
|
style="width: 100%;"
|
||||||
clearable
|
clearable
|
||||||
|
>
|
||||||
|
<template #default="{item}">
|
||||||
|
<div class="value">{{ item.value }}</div>
|
||||||
|
</template>
|
||||||
|
</el-autocomplete>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item key="authType" label="认证方式" prop="authType">
|
||||||
|
<el-radio v-model.trim="hostForm.authType" value="privateKey">密钥</el-radio>
|
||||||
|
<el-radio v-model.trim="hostForm.authType" value="password">密码</el-radio>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
v-if="hostForm.authType === 'privateKey'"
|
||||||
|
key="privateKey"
|
||||||
|
prop="privateKey"
|
||||||
|
label="密钥"
|
||||||
|
>
|
||||||
|
<el-button type="primary" size="small" @click="handleClickUploadBtn">
|
||||||
|
本地私钥...
|
||||||
|
</el-button>
|
||||||
|
<!-- <el-button type="primary" size="small" @click="handleClickUploadBtn">
|
||||||
|
从凭据导入...
|
||||||
|
</el-button> -->
|
||||||
|
<input
|
||||||
|
ref="privateKeyRef"
|
||||||
|
type="file"
|
||||||
|
name="privateKey"
|
||||||
|
style="display: none;"
|
||||||
|
@change="handleSelectPrivateKeyFile"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model.trim="hostForm.privateKey"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
clearable
|
||||||
|
autocomplete="off"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
placeholder="-----BEGIN RSA PRIVATE KEY-----"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
v-if="hostForm.authType === 'password'"
|
||||||
|
key="password"
|
||||||
|
prop="password"
|
||||||
|
label="密码"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model.trim="hostForm.password"
|
||||||
|
type="password"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
clearable
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item key="command" prop="command" label="执行指令">
|
||||||
|
<el-input
|
||||||
|
v-model="hostForm.command"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
clearable
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="连接服务器后自动执行的指令(例如: sudo -i)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="expired" label="到期时间" prop="expired">
|
<el-form-item key="expired" label="到期时间" prop="expired">
|
||||||
@ -57,7 +147,7 @@
|
|||||||
type="date"
|
type="date"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
value-format="x"
|
value-format="x"
|
||||||
placeholder="服务器到期时间"
|
placeholder="实例到期时间"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
@ -66,7 +156,7 @@
|
|||||||
label="到期提醒"
|
label="到期提醒"
|
||||||
prop="expiredNotify"
|
prop="expiredNotify"
|
||||||
>
|
>
|
||||||
<el-tooltip content="将在服务器到期前7、3、1天发送提醒(需在设置中绑定有效邮箱)" placement="right">
|
<el-tooltip content="将在实例到期前7、3、1天发送提醒(需在设置中绑定有效邮箱)" placement="right">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="hostForm.expiredNotify"
|
v-model="hostForm.expiredNotify"
|
||||||
:active-value="true"
|
:active-value="true"
|
||||||
@ -74,14 +164,6 @@
|
|||||||
/>
|
/>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="index" label="序号" prop="index">
|
|
||||||
<el-input
|
|
||||||
v-model.trim.number="hostForm.index"
|
|
||||||
clearable
|
|
||||||
placeholder="用于服务器列表中排序(填写数字)"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item key="consoleUrl" label="控制台URL" prop="consoleUrl">
|
<el-form-item key="consoleUrl" label="控制台URL" prop="consoleUrl">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="hostForm.consoleUrl"
|
v-model.trim="hostForm.consoleUrl"
|
||||||
@ -91,6 +173,14 @@
|
|||||||
@keyup.enter="handleSave"
|
@keyup.enter="handleSave"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item key="index" label="序号" prop="index">
|
||||||
|
<el-input
|
||||||
|
v-model.trim.number="hostForm.index"
|
||||||
|
clearable
|
||||||
|
placeholder="用于实例列表中排序(填写数字)"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item key="remark" label="备注" prop="remark">
|
<el-form-item key="remark" label="备注" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="hostForm.remark"
|
v-model.trim="hostForm.remark"
|
||||||
@ -98,7 +188,7 @@
|
|||||||
:rows="3"
|
:rows="3"
|
||||||
clearable
|
clearable
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="简单记录服务器用途"
|
placeholder="简单记录实例用途"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
@ -134,24 +224,36 @@ const resetForm = () => ({
|
|||||||
group: 'default',
|
group: 'default',
|
||||||
name: '',
|
name: '',
|
||||||
host: '',
|
host: '',
|
||||||
|
port: 22,
|
||||||
|
username: 'root',
|
||||||
|
authType: 'privateKey',
|
||||||
|
password: '',
|
||||||
|
privateKey: '',
|
||||||
index: 0,
|
index: 0,
|
||||||
expired: null,
|
expired: null,
|
||||||
expiredNotify: false,
|
expiredNotify: false,
|
||||||
consoleUrl: '',
|
consoleUrl: '',
|
||||||
remark: ''
|
remark: '',
|
||||||
|
command: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const hostForm = reactive(resetForm())
|
const hostForm = reactive(resetForm())
|
||||||
|
const privateKeyRef = ref(null)
|
||||||
const oldHost = ref('')
|
const oldHost = ref('')
|
||||||
const rules = reactive({
|
const rules = computed(() => {
|
||||||
group: { required: true, message: '选择一个分组' },
|
return {
|
||||||
name: { required: true, message: '输入主机别名', trigger: 'change' },
|
group: { required: true, message: '选择一个分组' },
|
||||||
host: { required: true, message: '输入IP/域名', trigger: 'change' },
|
name: { required: true, message: '输入实例别名', trigger: 'change' },
|
||||||
index: { required: true, type: 'number', message: '输入数字', trigger: 'change' },
|
host: { required: true, message: '输入IP/域名', trigger: 'change' },
|
||||||
expired: { required: false },
|
port: { required: true, type: 'number', message: '输入ssh端口', trigger: 'change' },
|
||||||
expiredNotify: { required: false },
|
index: { required: true, type: 'number', message: '输入数字', trigger: 'change' },
|
||||||
consoleUrl: { required: false },
|
// password: [{ required: hostForm.authType === 'password', trigger: 'change' },],
|
||||||
remark: { required: false }
|
// privateKey: [{ required: hostForm.authType === 'privateKey', trigger: 'change' },],
|
||||||
|
expired: { required: false },
|
||||||
|
expiredNotify: { required: false, type: 'boolean' },
|
||||||
|
consoleUrl: { required: false },
|
||||||
|
remark: { required: false }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
@ -161,7 +263,7 @@ const visible = computed({
|
|||||||
set: (newVal) => emit('update:show', newVal)
|
set: (newVal) => emit('update:show', newVal)
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = computed(() => props.defaultData ? '修改服务器' : '新增服务器')
|
const title = computed(() => props.defaultData ? '修改实例' : '新增实例')
|
||||||
|
|
||||||
let groupList = computed(() => $store.groupList || [])
|
let groupList = computed(() => $store.groupList || [])
|
||||||
|
|
||||||
@ -174,19 +276,45 @@ const handleClosed = () => {
|
|||||||
|
|
||||||
const setDefaultData = () => {
|
const setDefaultData = () => {
|
||||||
if (!props.defaultData) return
|
if (!props.defaultData) return
|
||||||
// console.log(props.defaultData)
|
let { host } = props.defaultData
|
||||||
let { name, host, index, expired, expiredNotify, consoleUrl, group, remark } = props.defaultData
|
|
||||||
oldHost.value = host
|
oldHost.value = host
|
||||||
Object.assign(hostForm, { name, host, index, expired, expiredNotify, consoleUrl, group, remark })
|
Object.assign(hostForm, { ...props.defaultData })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickUploadBtn = () => {
|
||||||
|
privateKeyRef.value.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectPrivateKeyFile = (event) => {
|
||||||
|
let file = event.target.files[0]
|
||||||
|
let reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
hostForm.privateKey = e.target.result
|
||||||
|
privateKeyRef.value.value = ''
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultUsers = [
|
||||||
|
{ value: 'root' },
|
||||||
|
{ value: 'debian' },
|
||||||
|
{ value: 'centos' },
|
||||||
|
{ value: 'ubuntu' },
|
||||||
|
{ value: 'azureuser' },
|
||||||
|
{ value: 'ec2-user' },
|
||||||
|
{ value: 'opc' },
|
||||||
|
{ value: 'admin' },
|
||||||
|
]
|
||||||
|
const userSearch = (keyword, cb) => {
|
||||||
|
let res = keyword
|
||||||
|
? defaultUsers.filter((item) => item.value.includes(keyword))
|
||||||
|
: defaultUsers
|
||||||
|
cb(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
formRef.value.validate()
|
formRef.value.validate()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
if (!hostForm.expired || !hostForm.expiredNotify) {
|
|
||||||
hostForm.expired = null
|
|
||||||
hostForm.expiredNotify = false
|
|
||||||
}
|
|
||||||
if (props.defaultData) {
|
if (props.defaultData) {
|
||||||
let { msg } = await $api.updateHost(Object.assign({}, hostForm, { oldHost: oldHost.value }))
|
let { msg } = await $api.updateHost(Object.assign({}, hostForm, { oldHost: oldHost.value }))
|
||||||
$message({ type: 'success', center: true, message: msg })
|
$message({ type: 'success', center: true, message: msg })
|
||||||
@ -202,6 +330,12 @@ const handleSave = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.instance_info {
|
||||||
|
display: flex;
|
||||||
|
.form_item {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -1,185 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-loading="loading">
|
|
||||||
<el-form
|
|
||||||
ref="emailFormRef"
|
|
||||||
:model="emailForm"
|
|
||||||
:rules="rules"
|
|
||||||
:inline="true"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
>
|
|
||||||
<el-form-item label="" prop="target" style="width: 200px;">
|
|
||||||
<el-select
|
|
||||||
v-model="emailForm.target"
|
|
||||||
placeholder="邮件服务商"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in supportEmailList"
|
|
||||||
:key="item.target"
|
|
||||||
:label="item.name"
|
|
||||||
:value="item.target"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="" prop="auth.user" style="width: 200px;">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="emailForm.auth.user"
|
|
||||||
clearable
|
|
||||||
placeholder="邮箱"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="" prop="auth.pass" style="width: 200px;">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="emailForm.auth.pass"
|
|
||||||
clearable
|
|
||||||
placeholder="SMTP授权码"
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="addEmail"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="">
|
|
||||||
<el-tooltip
|
|
||||||
effect="dark"
|
|
||||||
content="重复添加的邮箱将会被覆盖"
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<el-button type="primary" @click="addEmail">
|
|
||||||
添加
|
|
||||||
</el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<!-- 提示 -->
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;"> Tips: 系统所有通知邮件将会下发到所有已经配置成功的邮箱中 </span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
<!-- 表格 -->
|
|
||||||
<el-table :data="userEmailList" class="table">
|
|
||||||
<el-table-column prop="email" label="Email" />
|
|
||||||
<el-table-column prop="name" label="服务商" />
|
|
||||||
<el-table-column label="操作">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:loading="row.loading"
|
|
||||||
@click="pushTestEmail(row)"
|
|
||||||
>
|
|
||||||
测试
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
@click="deleteUserEmail(row)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $messageBox, $notification } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const userEmailList = ref([])
|
|
||||||
const supportEmailList = ref([])
|
|
||||||
const emailFormRef = ref(null)
|
|
||||||
|
|
||||||
const emailForm = reactive({
|
|
||||||
target: 'qq',
|
|
||||||
auth: {
|
|
||||||
user: '',
|
|
||||||
pass: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const rules = reactive({
|
|
||||||
'auth.user': { required: true, type: 'email', message: '需输入邮箱', trigger: 'change' },
|
|
||||||
'auth.pass': { required: true, message: '需输入SMTP授权码', trigger: 'change' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const getUserEmailList = () => {
|
|
||||||
loading.value = true
|
|
||||||
$api.getUserEmailList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
userEmailList.value = data.map(item => {
|
|
||||||
item.loading = false
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => loading.value = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSupportEmailList = () => {
|
|
||||||
$api.getSupportEmailList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
supportEmailList.value = data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const addEmail = () => {
|
|
||||||
emailFormRef.value.validate()
|
|
||||||
.then(() => {
|
|
||||||
$api.updateUserEmailList({ ...emailForm })
|
|
||||||
.then(() => {
|
|
||||||
$message.success('添加成功, 点击[测试]按钮发送测试邮件')
|
|
||||||
let { target } = emailForm
|
|
||||||
emailForm.target = target
|
|
||||||
emailForm.auth.user = ''
|
|
||||||
emailForm.auth.pass = ''
|
|
||||||
getUserEmailList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const pushTestEmail = (row) => {
|
|
||||||
row.loading = true
|
|
||||||
const { email: toEmail } = row
|
|
||||||
$api.pushTestEmail({ isTest: true, toEmail })
|
|
||||||
.then(() => {
|
|
||||||
$message.success(`发送成功, 请检查邮箱: ${ toEmail }`)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
$notification({
|
|
||||||
title: '发送测试邮件失败, 请检查邮箱SMTP配置',
|
|
||||||
message: error.response?.data.msg,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
row.loading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteUserEmail = ({ email }) => {
|
|
||||||
$messageBox.confirm(
|
|
||||||
`确认删除邮箱:${ email }`,
|
|
||||||
'Warning',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(async () => {
|
|
||||||
await $api.deleteUserEmail(email)
|
|
||||||
$message.success('success')
|
|
||||||
getUserEmailList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getUserEmailList()
|
|
||||||
getSupportEmailList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,239 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-form
|
|
||||||
ref="groupFormRef"
|
|
||||||
:model="groupForm"
|
|
||||||
:rules="rules"
|
|
||||||
:inline="true"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
>
|
|
||||||
<el-form-item label="" prop="name" style="width: 200px;">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="groupForm.name"
|
|
||||||
clearable
|
|
||||||
placeholder="分组名称"
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="addGroup"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="" prop="index" style="width: 200px;">
|
|
||||||
<el-input
|
|
||||||
v-model.number="groupForm.index"
|
|
||||||
clearable
|
|
||||||
placeholder="序号(数字, 用于分组排序)"
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="addGroup"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="">
|
|
||||||
<el-button type="primary" @click="addGroup">
|
|
||||||
添加
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<!-- 提示 -->
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;">
|
|
||||||
Tips: 已添加服务器数量 <u>{{ hostGroupInfo.total }}</u>
|
|
||||||
<span v-show="hostGroupInfo.notGroupCount">, 有 <u>{{ hostGroupInfo.notGroupCount }}</u> 台服务器尚未分组</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-alert><br>
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;"> Tips: 删除分组会将分组内所有服务器移至默认分组 </span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
<el-table v-loading="loading" :data="list">
|
|
||||||
<el-table-column prop="index" label="序号" />
|
|
||||||
<el-table-column prop="id" label="ID" />
|
|
||||||
<el-table-column prop="name" label="分组名称" />
|
|
||||||
<el-table-column label="关联服务器数量">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-popover
|
|
||||||
v-if="row.hosts.list.length !== 0"
|
|
||||||
placement="right"
|
|
||||||
:width="350"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #reference>
|
|
||||||
<u class="host-count">{{ row.hosts.count }}</u>
|
|
||||||
</template>
|
|
||||||
<ul>
|
|
||||||
<li v-for="item in row.hosts.list" :key="item.host">
|
|
||||||
<span>{{ item.host }}</span>
|
|
||||||
-
|
|
||||||
<span>{{ item.name }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</el-popover>
|
|
||||||
<u v-else class="host-count">0</u>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button type="primary" @click="handleChange(row)">修改</el-button>
|
|
||||||
<el-button v-show="row.id !== 'default'" type="danger" @click="deleteGroup(row)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
width="400px"
|
|
||||||
title="修改分组"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="updateFormRef"
|
|
||||||
:model="updateForm"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="100px"
|
|
||||||
>
|
|
||||||
<el-form-item label="分组名称" prop="name">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="updateForm.name"
|
|
||||||
clearable
|
|
||||||
placeholder="分组名称"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="分组序号" prop="index">
|
|
||||||
<el-input
|
|
||||||
v-model.number="updateForm.index"
|
|
||||||
clearable
|
|
||||||
placeholder="分组序号"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="visible = false">关闭</el-button>
|
|
||||||
<el-button type="primary" @click="updateGroup">修改</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const visible = ref(false)
|
|
||||||
const groupList = ref([])
|
|
||||||
const groupForm = reactive({
|
|
||||||
name: '',
|
|
||||||
index: ''
|
|
||||||
})
|
|
||||||
const updateForm = reactive({
|
|
||||||
name: '',
|
|
||||||
index: ''
|
|
||||||
})
|
|
||||||
const rules = reactive({
|
|
||||||
name: { required: true, message: '需输入分组名称', trigger: 'change' },
|
|
||||||
index: { required: true, type: 'number', message: '需输入数字', trigger: 'change' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const groupFormRef = ref(null)
|
|
||||||
const updateFormRef = ref(null)
|
|
||||||
|
|
||||||
const hostGroupInfo = computed(() => {
|
|
||||||
const total = $store.hostList.length
|
|
||||||
const notGroupCount = $store.hostList.reduce((prev, next) => {
|
|
||||||
if (!next.group) prev++
|
|
||||||
return prev
|
|
||||||
}, 0)
|
|
||||||
return { total, notGroupCount }
|
|
||||||
})
|
|
||||||
|
|
||||||
const list = computed(() => {
|
|
||||||
return groupList.value.map(item => {
|
|
||||||
const hosts = $store.hostList.reduce((prev, next) => {
|
|
||||||
if (next.group === item.id) {
|
|
||||||
prev.count++
|
|
||||||
prev.list.push(next)
|
|
||||||
}
|
|
||||||
return prev
|
|
||||||
}, { count: 0, list: [] })
|
|
||||||
return { ...item, hosts }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const getGroupList = () => {
|
|
||||||
loading.value = true
|
|
||||||
$api.getGroupList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
groupList.value = data
|
|
||||||
groupForm.index = data.length
|
|
||||||
})
|
|
||||||
.finally(() => loading.value = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addGroup = () => {
|
|
||||||
groupFormRef.value.validate()
|
|
||||||
.then(() => {
|
|
||||||
const { name, index } = groupForm
|
|
||||||
$api.addGroup({ name, index })
|
|
||||||
.then(() => {
|
|
||||||
$message.success('success')
|
|
||||||
groupForm.name = ''
|
|
||||||
groupForm.index = ''
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = ({ id, name, index }) => {
|
|
||||||
updateForm.id = id
|
|
||||||
updateForm.name = name
|
|
||||||
updateForm.index = index
|
|
||||||
visible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGroup = () => {
|
|
||||||
updateFormRef.value.validate()
|
|
||||||
.then(() => {
|
|
||||||
const { id, name, index } = updateForm
|
|
||||||
$api.updateGroup(id, { name, index })
|
|
||||||
.then(() => {
|
|
||||||
$message.success('success')
|
|
||||||
visible.value = false
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteGroup = ({ id, name }) => {
|
|
||||||
$messageBox.confirm(`确认删除分组:${ name }`, 'Warning', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await $api.deleteGroup(id)
|
|
||||||
await $store.getHostList()
|
|
||||||
$message.success('success')
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getGroupList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.host-count {
|
|
||||||
display: block;
|
|
||||||
width: 100px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #87cf63;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;"> Tips: 请添加邮箱并确保测试邮件通过 </span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
<el-table v-loading="notifyListLoading" :data="notifyList">
|
|
||||||
<el-table-column prop="desc" label="通知类型" />
|
|
||||||
<el-table-column prop="sw" label="开关">
|
|
||||||
<template #default="{row}">
|
|
||||||
<el-switch
|
|
||||||
v-model="row.sw"
|
|
||||||
:active-value="true"
|
|
||||||
:inactive-value="false"
|
|
||||||
:loading="row.loading"
|
|
||||||
@change="handleChangeSw(row, $event)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const notifyListLoading = ref(false)
|
|
||||||
const notifyList = ref([])
|
|
||||||
|
|
||||||
const getNotifyList = (flag = true) => {
|
|
||||||
if (flag) notifyListLoading.value = true
|
|
||||||
$api.getNotifyList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
notifyList.value = data.map((item) => {
|
|
||||||
item.loading = false
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => notifyListLoading.value = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeSw = async (row) => {
|
|
||||||
row.loading = true
|
|
||||||
const { type, sw } = row
|
|
||||||
try {
|
|
||||||
await $api.updateNotifyList({ type, sw })
|
|
||||||
// if (this.userEmailList.length === 0) $message.warning('未配置邮箱, 此开关将不会生效')
|
|
||||||
} finally {
|
|
||||||
row.loading = false
|
|
||||||
}
|
|
||||||
getNotifyList(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getNotifyList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,98 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
class="password-form"
|
|
||||||
:model="formData"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="90px"
|
|
||||||
:show-message="false"
|
|
||||||
>
|
|
||||||
<el-form-item label="原用户名" prop="oldLoginName">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.oldLoginName"
|
|
||||||
clearable
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="原密码" prop="oldPwd">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.oldPwd"
|
|
||||||
type="password"
|
|
||||||
clearable
|
|
||||||
show-password
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="新用户名" prop="oldPwd">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.newLoginName"
|
|
||||||
clearable
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="新密码" prop="newPwd">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.newPwd"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
@keyup.enter="handleUpdate"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" :loading="loading" @click="handleUpdate">确认</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, getCurrentInstance } from 'vue'
|
|
||||||
import { RSAEncrypt } from '@utils/index.js'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const formRef = ref(null)
|
|
||||||
const formData = reactive({
|
|
||||||
oldLoginName: '',
|
|
||||||
oldPwd: '',
|
|
||||||
newLoginName: '',
|
|
||||||
newPwd: ''
|
|
||||||
})
|
|
||||||
const rules = reactive({
|
|
||||||
oldLoginName: { required: true, message: '输入原用户名', trigger: 'change' },
|
|
||||||
oldPwd: { required: true, message: '输入原密码', trigger: 'change' },
|
|
||||||
newLoginName: { required: true, message: '输入新用户名', trigger: 'change' },
|
|
||||||
newPwd: { required: true, message: '输入新密码', trigger: 'change' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleUpdate = () => {
|
|
||||||
formRef.value.validate()
|
|
||||||
.then(async () => {
|
|
||||||
let { oldLoginName, oldPwd, newLoginName, newPwd } = formData
|
|
||||||
oldPwd = RSAEncrypt(oldPwd)
|
|
||||||
newPwd = RSAEncrypt(newPwd)
|
|
||||||
let { msg } = await $api.updatePwd({ oldLoginName, oldPwd, newLoginName, newPwd })
|
|
||||||
$message({ type: 'success', center: true, message: msg })
|
|
||||||
$store.setUser(newLoginName)
|
|
||||||
formData.oldLoginName = ''
|
|
||||||
formData.oldPwd = ''
|
|
||||||
formData.newLoginName = ''
|
|
||||||
formData.newPwd = ''
|
|
||||||
formRef.value.resetFields()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.password-form {
|
|
||||||
width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,46 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-alert type="success" :closable="false">
|
|
||||||
<template #title>
|
|
||||||
<span style="letter-spacing: 2px;"> Tips: 系统只保存最近10条登录记录, 检测到更换IP后需重新登录 </span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
<el-table v-loading="loading" :data="loginRecordList">
|
|
||||||
<el-table-column prop="ip" label="IP" />
|
|
||||||
<el-table-column prop="address" label="地点" show-overflow-tooltip>
|
|
||||||
<template #default="scope">
|
|
||||||
<span style="letter-spacing: 2px;"> {{ scope.row.country }} {{ scope.row.city }} </span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="date" label="时间" />
|
|
||||||
</el-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $tools } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const loginRecordList = ref([])
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const handleLookupLoginRecord = () => {
|
|
||||||
loading.value = true
|
|
||||||
$api.getLoginRecord()
|
|
||||||
.then(({ data }) => {
|
|
||||||
loginRecordList.value = data.map((item) => {
|
|
||||||
item.date = $tools.formatTimestamp(item.date)
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
handleLookupLoginRecord()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
</style>
|
|
@ -1,98 +0,0 @@
|
|||||||
<template>
|
|
||||||
<transition-group
|
|
||||||
name="list"
|
|
||||||
mode="out-in"
|
|
||||||
tag="ul"
|
|
||||||
class="host-list"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(item, index) in list"
|
|
||||||
:key="item.host"
|
|
||||||
:draggable="true"
|
|
||||||
class="host-item"
|
|
||||||
@dragenter="dragenter($event, index)"
|
|
||||||
@dragover="dragover($event)"
|
|
||||||
@dragstart="dragstart(index)"
|
|
||||||
>
|
|
||||||
<span>{{ item.host }}</span>
|
|
||||||
---
|
|
||||||
<span>{{ item.name }}</span>
|
|
||||||
</li>
|
|
||||||
</transition-group>
|
|
||||||
<div style="display: flex; justify-content: center; margin-top: 25px">
|
|
||||||
<el-button type="primary" @click="handleUpdateSort">
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const emit = defineEmits(['update-list',])
|
|
||||||
const { proxy: { $api, $message, $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const targetIndex = ref(0)
|
|
||||||
const list = ref([])
|
|
||||||
|
|
||||||
const dragstart = (index) => {
|
|
||||||
targetIndex.value = index
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragenter = (e, curIndex) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (targetIndex.value !== curIndex) {
|
|
||||||
let target = list.value.splice(targetIndex.value, 1)[0]
|
|
||||||
list.value.splice(curIndex, 0, target)
|
|
||||||
targetIndex.value = curIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragover = (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateSort = () => {
|
|
||||||
$api.updateHostSort({ list: list.value })
|
|
||||||
.then(({ msg }) => {
|
|
||||||
$message({ type: 'success', center: true, message: msg })
|
|
||||||
$store.sortHostList(list.value)
|
|
||||||
emit('update-list', list.value) // 触发自定义事件
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
list.value = $store.hostList.map(({ name, host }) => ({ name, host }))
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.drag-move {
|
|
||||||
transition: transform .3s;
|
|
||||||
}
|
|
||||||
.host-list {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-right: 50px;
|
|
||||||
}
|
|
||||||
.host-item {
|
|
||||||
transition: all .3s;
|
|
||||||
box-shadow: var(--el-box-shadow-lighter);
|
|
||||||
cursor: move;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #595959;
|
|
||||||
padding: 0 20px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #000;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
height: 35px;
|
|
||||||
line-height: 35px;
|
|
||||||
&:hover {
|
|
||||||
box-shadow: var(--el-box-shadow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,69 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
width="1100px"
|
|
||||||
:title="'功能设置'"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
:close-on-press-escape="false"
|
|
||||||
>
|
|
||||||
<el-tabs style="height: 500px;" tab-position="left">
|
|
||||||
<el-tab-pane label="分组管理">
|
|
||||||
<Group />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="登录记录">
|
|
||||||
<Record />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="主机排序" lazy>
|
|
||||||
<Sort @update-list="emitUpdateList" />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="全局通知" lazy>
|
|
||||||
<NotifyList />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="邮箱配置" lazy>
|
|
||||||
<EmailList />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="修改密码" lazy>
|
|
||||||
<Password />
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import NotifyList from './setting-tab/notify-list.vue'
|
|
||||||
import EmailList from './setting-tab/email-list.vue'
|
|
||||||
import Sort from './setting-tab/sort.vue'
|
|
||||||
import Record from './setting-tab/record.vue'
|
|
||||||
import Group from './setting-tab/group.vue'
|
|
||||||
import Password from './setting-tab/password.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
show: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:show', 'update-list',])
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.show,
|
|
||||||
set: (newVal) => emit('update:show', newVal)
|
|
||||||
})
|
|
||||||
|
|
||||||
const emitUpdateList = () => {
|
|
||||||
emit('update-list')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.table {
|
|
||||||
max-height: 400px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -13,7 +13,7 @@
|
|||||||
label-suffix=":"
|
label-suffix=":"
|
||||||
label-width="90px"
|
label-width="90px"
|
||||||
>
|
>
|
||||||
<el-form-item label="主机" prop="host">
|
<el-form-item label="实例" prop="host">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="sshForm.host"
|
v-model.trim="sshForm.host"
|
||||||
disabled
|
disabled
|
||||||
@ -52,7 +52,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="sshForm.type === 'privateKey'" prop="privateKey" label="密钥">
|
<el-form-item v-if="sshForm.type === 'privateKey'" prop="privateKey" label="密钥">
|
||||||
<el-button type="primary" size="small" @click="handleClickUploadBtn">
|
<el-button type="primary" size="small" @click="handleClickUploadBtn">
|
||||||
选择私钥...
|
本地私钥...
|
||||||
</el-button>
|
</el-button>
|
||||||
<input
|
<input
|
||||||
ref="privateKeyRef"
|
ref="privateKeyRef"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="server_group_container">
|
<div class="server_group_container">
|
||||||
<div class="server_group_header">
|
<div class="server_group_header">
|
||||||
<el-button type="primary" @click="hostFormVisible = true">添加服务器</el-button>
|
<el-button type="primary" @click="hostFormVisible = true">添加实例</el-button>
|
||||||
<el-button type="primary" @click="handleHiddenIP">
|
<el-button type="primary" @click="handleHiddenIP">
|
||||||
{{ hiddenIp ? '显示IP' : '隐藏IP' }}
|
{{ hiddenIp ? '显示IP' : '隐藏IP' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -20,8 +20,8 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
:host-info="item"
|
:host-info="item"
|
||||||
:hidden-ip="hiddenIp"
|
:hidden-ip="hiddenIp"
|
||||||
@update-list="handleUpdateList"
|
|
||||||
@update-host="handleUpdateHost"
|
@update-host="handleUpdateHost"
|
||||||
|
@update-list="handleUpdateList"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
@ -39,8 +39,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onBeforeUnmount, getCurrentInstance, computed, watch, onMounted } from 'vue'
|
import { ref, onBeforeUnmount, getCurrentInstance, computed, watch, onMounted } from 'vue'
|
||||||
import { io } from 'socket.io-client'
|
import { io } from 'socket.io-client'
|
||||||
import HostForm from './components/host-form.vue'
|
|
||||||
import HostCard from './components/host-card.vue'
|
import HostCard from './components/host-card.vue'
|
||||||
|
import HostForm from './components/host-form.vue'
|
||||||
|
|
||||||
const { proxy: { $store, $notification, $router, $serviceURI, $message } } = getCurrentInstance()
|
const { proxy: { $store, $notification, $router, $serviceURI, $message } } = getCurrentInstance()
|
||||||
|
|
||||||
@ -50,52 +50,13 @@ const hostFormVisible = ref(false)
|
|||||||
const hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
const hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
||||||
const activeGroup = ref([])
|
const activeGroup = ref([])
|
||||||
|
|
||||||
const connectIo = () => {
|
|
||||||
if (socket.value) socket.value.close()
|
|
||||||
let socketInstance = io($serviceURI, {
|
|
||||||
path: '/clients',
|
|
||||||
forceNew: true,
|
|
||||||
reconnectionDelay: 5000,
|
|
||||||
reconnectionAttempts: 2
|
|
||||||
})
|
|
||||||
socket.value = socketInstance
|
|
||||||
socketInstance.on('connect', () => {
|
|
||||||
let flag = 5
|
|
||||||
console.log('clients websocket 已连接: ', socketInstance.id)
|
|
||||||
let token = $store.token
|
|
||||||
socketInstance.emit('init_clients_data', { token })
|
|
||||||
socketInstance.on('clients_data', (data) => {
|
|
||||||
if ((flag++ % 5) === 0) $store.getHostPing()
|
|
||||||
$store.hostList.forEach(item => {
|
|
||||||
const { host } = item
|
|
||||||
if (data[host] === null) return { ...item }
|
|
||||||
return Object.assign(item, data[host])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
socketInstance.on('token_verify_fail', (message) => {
|
|
||||||
$notification({
|
|
||||||
title: '鉴权失败',
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
$router.push('/login')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
socketInstance.on('disconnect', () => {
|
|
||||||
console.error('clients websocket 连接断开')
|
|
||||||
})
|
|
||||||
socketInstance.on('connect_error', (message) => {
|
|
||||||
console.error('clients websocket 连接出错: ', message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateList = async () => {
|
const handleUpdateList = async () => {
|
||||||
try {
|
try {
|
||||||
await $store.getHostList()
|
await $store.getHostList()
|
||||||
// connectIo()
|
connectIo()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$message.error('获取主机列表失败')
|
$message.error('获取实例列表失败')
|
||||||
console.error('获取主机列表失败: ', err)
|
console.error('获取实例列表失败: ', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,10 +98,56 @@ watch(groupHostList, () => {
|
|||||||
deep: false
|
deep: false
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
let hostList = computed(() => $store.hostList)
|
||||||
|
|
||||||
|
const unwatchHost = watch(hostList, () => {
|
||||||
connectIo()
|
connectIo()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const connectIo = () => {
|
||||||
|
if (socket.value) socket.value.close()
|
||||||
|
unwatchHost()
|
||||||
|
let socketInstance = io($serviceURI, {
|
||||||
|
path: '/clients',
|
||||||
|
forceNew: true,
|
||||||
|
reconnectionDelay: 5000,
|
||||||
|
reconnectionAttempts: 2
|
||||||
|
})
|
||||||
|
socket.value = socketInstance
|
||||||
|
socketInstance.on('connect', () => {
|
||||||
|
let flag = 5
|
||||||
|
console.log('clients websocket 已连接: ', socketInstance.id)
|
||||||
|
let token = $store.token
|
||||||
|
socketInstance.emit('init_clients_data', { token })
|
||||||
|
socketInstance.on('clients_data', (data) => {
|
||||||
|
if ((flag++ % 5) === 0) $store.getHostPing()
|
||||||
|
$store.hostList.forEach(item => {
|
||||||
|
const { host } = item
|
||||||
|
if (data[host] === null) return { ...item }
|
||||||
|
return Object.assign(item, data[host])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
socketInstance.on('token_verify_fail', (message) => {
|
||||||
|
$notification({
|
||||||
|
title: '鉴权失败',
|
||||||
|
message,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
$router.push('/login')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
socketInstance.on('disconnect', () => {
|
||||||
|
console.error('clients websocket 连接断开')
|
||||||
|
})
|
||||||
|
socketInstance.on('connect_error', (message) => {
|
||||||
|
console.error('clients websocket 连接出错: ', message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// onMounted(() => {
|
||||||
|
// connectIo()
|
||||||
|
// })
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (socket.value) socket.value.close()
|
if (socket.value) socket.value.close()
|
||||||
})
|
})
|
||||||
|
@ -35,21 +35,21 @@
|
|||||||
<el-alert type="success" :closable="false">
|
<el-alert type="success" :closable="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span style="letter-spacing: 2px;">
|
<span style="letter-spacing: 2px;">
|
||||||
Tips: 已添加服务器数量 <u>{{ hostGroupInfo.total }}</u>
|
Tips: 已添加实例数量 <u>{{ hostGroupInfo.total }}</u>
|
||||||
<span v-show="hostGroupInfo.notGroupCount">, 有 <u>{{ hostGroupInfo.notGroupCount }}</u> 台服务器尚未分组</span>
|
<span v-show="hostGroupInfo.notGroupCount">, 有 <u>{{ hostGroupInfo.notGroupCount }}</u> 台实例尚未分组</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-alert><br>
|
</el-alert><br>
|
||||||
<el-alert type="success" :closable="false">
|
<el-alert type="success" :closable="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span style="letter-spacing: 2px;"> Tips: 删除分组会将分组内所有服务器移至默认分组 </span>
|
<span style="letter-spacing: 2px;"> Tips: 删除分组会将分组内所有实例移至默认分组 </span>
|
||||||
</template>
|
</template>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<el-table v-loading="loading" :data="list">
|
<el-table v-loading="loading" :data="list">
|
||||||
<el-table-column prop="index" label="序号" />
|
<el-table-column prop="index" label="序号" />
|
||||||
<el-table-column prop="id" label="ID" />
|
<el-table-column prop="id" label="ID" />
|
||||||
<el-table-column prop="name" label="分组名称" />
|
<el-table-column prop="name" label="分组名称" />
|
||||||
<el-table-column label="关联服务器数量">
|
<el-table-column label="关联实例数量">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-popover
|
<el-popover
|
||||||
v-if="row.hosts.list.length !== 0"
|
v-if="row.hosts.list.length !== 0"
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<el-tab-pane label="登录日志">
|
<el-tab-pane label="登录日志">
|
||||||
<Record />
|
<Record />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<!-- <el-tab-pane label="主机排序" lazy>
|
<!-- <el-tab-pane label="实例排序" lazy>
|
||||||
<Sort @update-list="emitUpdateList" />
|
<Sort @update-list="emitUpdateList" />
|
||||||
</el-tab-pane> -->
|
</el-tab-pane> -->
|
||||||
<el-tab-pane label="全局通知" lazy>
|
<el-tab-pane label="全局通知" lazy>
|
||||||
|
@ -52,16 +52,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onBeforeMount, getCurrentInstance } from 'vue'
|
import { ref, reactive, computed, onBeforeMount, getCurrentInstance } from 'vue'
|
||||||
import TerminalTab from './components/terminal-tab.vue'
|
import TerminalTab from './terminal-tab.vue'
|
||||||
import InfoSide from './components/info-side.vue'
|
import InfoSide from './info-side.vue'
|
||||||
import SftpFooter from './components/sftp-footer.vue'
|
import SftpFooter from './sftp-footer.vue'
|
||||||
import InputCommand from '@/components/input-command/index.vue'
|
import InputCommand from '@/components/input-command/index.vue'
|
||||||
|
|
||||||
const { proxy: { $store, $router, $route, $nextTick } } = getCurrentInstance()
|
const { proxy: { $store, $router, $route, $nextTick } } = getCurrentInstance()
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const host = ref('')
|
const host = ref('')
|
||||||
const token = $store.token
|
|
||||||
const activeTab = ref('')
|
const activeTab = ref('')
|
||||||
const terminalTabs = reactive([])
|
const terminalTabs = reactive([])
|
||||||
const isFullScreen = ref(false)
|
const isFullScreen = ref(false)
|
||||||
@ -71,11 +70,12 @@ const showInputCommand = ref(false)
|
|||||||
const visible = ref(true)
|
const visible = ref(true)
|
||||||
const infoSideRef = ref(null)
|
const infoSideRef = ref(null)
|
||||||
const terminalTabRefs = ref([])
|
const terminalTabRefs = ref([])
|
||||||
|
const token = computed(() => $store.token)
|
||||||
|
|
||||||
const closable = computed(() => terminalTabs.length > 1)
|
const closable = computed(() => terminalTabs.length > 1)
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
if (!token) return $router.push('login')
|
if (!token.value) return $router.push('login')
|
||||||
let { host: routeHost, name: routeName } = $route.query
|
let { host: routeHost, name: routeName } = $route.query
|
||||||
name.value = routeName
|
name.value = routeName
|
||||||
host.value = routeHost
|
host.value = routeHost
|
@ -3,30 +3,79 @@
|
|||||||
<div v-if="showLinkTips" class="terminal_link_tips">
|
<div v-if="showLinkTips" class="terminal_link_tips">
|
||||||
<h2 class="quick_link_text">快速连接</h2>
|
<h2 class="quick_link_text">快速连接</h2>
|
||||||
<el-table
|
<el-table
|
||||||
:data="hostList"
|
:data="tabelData"
|
||||||
:show-header="false"
|
:show-header="false"
|
||||||
>
|
>
|
||||||
<el-table-column prop="name" label="name" width="180" />
|
<el-table-column prop="name" label="name" />
|
||||||
<el-table-column prop="host" label="host" width="180" />
|
<el-table-column>
|
||||||
<el-table-column prop="address" label="Address" />
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.username ? `ssh ${row.username}@` : '' }}{{ row.host }}{{ row.port ? ` -p ${row.port}` : '' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-show="!isAllConfssh">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="actios_btns">
|
||||||
|
<el-button
|
||||||
|
v-if="row.username && row.port"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="linkTerminal(row)"
|
||||||
|
>
|
||||||
|
连接
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="success"
|
||||||
|
link
|
||||||
|
@click="confSSH(row)"
|
||||||
|
>
|
||||||
|
配置ssh
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<!-- <ul v-for="item in hostList" :key="item._id">
|
</div>
|
||||||
{{ item.name }}
|
<div v-else>
|
||||||
{{ item.host }}
|
<Terminal />
|
||||||
{{ item.name }}
|
|
||||||
</ul> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, getCurrentInstance } from 'vue'
|
import { ref, computed, getCurrentInstance } from 'vue'
|
||||||
|
import Terminal from './components/terminal.vue'
|
||||||
|
|
||||||
const { proxy: { $store } } = getCurrentInstance()
|
const { proxy: { $store } } = getCurrentInstance()
|
||||||
|
|
||||||
let showLinkTips = ref(true)
|
let showLinkTips = ref(true)
|
||||||
|
let ternimalTabs = ref([])
|
||||||
|
|
||||||
let hostList = computed(() => $store.hostList)
|
let hostList = computed(() => $store.hostList)
|
||||||
|
let sshList = computed(() => $store.sshList)
|
||||||
|
let tabelData = computed(() => {
|
||||||
|
return hostList.value.map(hostConf => {
|
||||||
|
// console.log(sshList.value)
|
||||||
|
let target = sshList.value?.find(sshConf => sshConf.host === hostConf.host)
|
||||||
|
if (target !== -1) {
|
||||||
|
return { ...hostConf, ...target }
|
||||||
|
}
|
||||||
|
return hostConf
|
||||||
|
})
|
||||||
|
})
|
||||||
|
let isAllConfssh = computed(() => {
|
||||||
|
return tabelData.value?.every(item => item.username && item.port)
|
||||||
|
})
|
||||||
|
|
||||||
|
function linkTerminal(row) {
|
||||||
|
// console.log(row)
|
||||||
|
ternimalTabs.value.push(row)
|
||||||
|
showLinkTips.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function confSSH(row) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -48,6 +97,10 @@ let hostList = computed(() => $store.hostList)
|
|||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
.actios_btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user