✨ 新增凭证管理功能&字段储存
This commit is contained in:
parent
5c3818dd73
commit
5b2b776155
@ -1,30 +1,45 @@
|
||||
const { readHostList, writeHostList } = require('../utils')
|
||||
const { readHostList, writeHostList, RSADecryptSync, AESEncryptSync, AESDecryptSync } = require('../utils')
|
||||
|
||||
async function getHostList({ res }) {
|
||||
// console.log('get-host-list')
|
||||
const data = await readHostList()
|
||||
let data = await readHostList()
|
||||
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
||||
data = data.map((item) => {
|
||||
const isConfig = Boolean(item.username && item.port && (item[item.authType]))
|
||||
return {
|
||||
...item,
|
||||
isConfig,
|
||||
password: '',
|
||||
privateKey: ''
|
||||
}
|
||||
})
|
||||
res.success({ data })
|
||||
}
|
||||
|
||||
async function saveHost({
|
||||
async function addHost({
|
||||
res, request
|
||||
}) {
|
||||
let {
|
||||
body: {
|
||||
name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||
port, username, authType, password, privateKey, command
|
||||
port, username, authType, password, privateKey, credential, command, tempKey
|
||||
}
|
||||
} = request
|
||||
// console.log(request)
|
||||
if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
|
||||
let hostList = await readHostList()
|
||||
if (hostList?.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
||||
if (!Array.isArray(hostList)) hostList = []
|
||||
hostList.push({
|
||||
host: newHost, name, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||
port, username, authType, password, privateKey, command
|
||||
})
|
||||
let record = {
|
||||
name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||
port, username, authType, password, privateKey, credential, command
|
||||
}
|
||||
const clearTempKey = await RSADecryptSync(tempKey)
|
||||
console.log('clearTempKey:', clearTempKey)
|
||||
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||
hostList.push(record)
|
||||
await writeHostList(hostList)
|
||||
res.success()
|
||||
}
|
||||
@ -35,17 +50,31 @@ async function updateHost({
|
||||
let {
|
||||
body: {
|
||||
host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark,
|
||||
port, username, authType, password, privateKey, command
|
||||
port, username, authType, password, privateKey, credential, command, tempKey
|
||||
}
|
||||
} = request
|
||||
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||
let hostList = await readHostList()
|
||||
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试新增实例` })
|
||||
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
||||
hostList.splice(targetIdx, 1, {
|
||||
let record = {
|
||||
name: newName, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||
port, username, authType, password, privateKey, command
|
||||
})
|
||||
port, username, authType, password, privateKey, credential, command
|
||||
}
|
||||
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试新增实例` })
|
||||
|
||||
let idx = hostList.findIndex(({ host }) => host === oldHost)
|
||||
const oldRecord = hostList[idx]
|
||||
// 如果存在原认证方式则保存下来
|
||||
if (!record[authType] && oldRecord[authType]) {
|
||||
record[authType] = oldRecord[authType]
|
||||
} else {
|
||||
const clearTempKey = await RSADecryptSync(tempKey)
|
||||
console.log('clearTempKey:', clearTempKey)
|
||||
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||
}
|
||||
hostList.splice(idx, 1, record)
|
||||
writeHostList(hostList)
|
||||
res.success()
|
||||
}
|
||||
@ -89,7 +118,7 @@ async function removeHost({
|
||||
|
||||
module.exports = {
|
||||
getHostList,
|
||||
saveHost,
|
||||
addHost,
|
||||
updateHost,
|
||||
removeHost
|
||||
// updateHostSort
|
||||
|
@ -1,52 +1,81 @@
|
||||
const { readSSHRecord, writeSSHRecord, AESEncryptSync } = require('../utils')
|
||||
const { readSSHRecord, writeSSHRecord, readHostList, writeHostList, RSADecryptSync, AESEncryptSync, AESDecryptSync } = 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 }
|
||||
const { name, authType, _id: id, date } = item
|
||||
return { id, name, authType, privateKey: '', password: '', date }
|
||||
}) || []
|
||||
data.sort((a, b) => b.date - a.date)
|
||||
res.success({ data })
|
||||
}
|
||||
|
||||
const updateSSH = async ({ res, request }) => {
|
||||
let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
|
||||
let record = { host, port, username, type, password, privateKey, randomKey, command }
|
||||
if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' })
|
||||
// 再做一次对称加密(方便ssh连接时解密)
|
||||
record.randomKey = await AESEncryptSync(randomKey)
|
||||
const addSSH = async ({ res, request }) => {
|
||||
let { body: { name, authType, password, privateKey, tempKey } } = request
|
||||
let record = { name, authType, password, privateKey }
|
||||
if(!name || !record[authType]) return res.fail({ data: false, msg: '参数错误' })
|
||||
let sshRecord = await readSSHRecord()
|
||||
let idx = sshRecord.findIndex(item => item.host === host)
|
||||
if(idx === -1)
|
||||
sshRecord.push(record)
|
||||
else
|
||||
sshRecord.splice(idx, 1, record)
|
||||
if (sshRecord.some(item => item.name === name)) return res.fail({ data: false, msg: '已存在同名凭证' })
|
||||
|
||||
const clearTempKey = await RSADecryptSync(tempKey)
|
||||
console.log('clearTempKey:', clearTempKey)
|
||||
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||
|
||||
sshRecord.push({ ...record, date: Date.now() })
|
||||
await writeSSHRecord(sshRecord)
|
||||
consola.info('新增凭证:', host)
|
||||
consola.info('新增凭证:', name)
|
||||
res.success({ data: '保存成功' })
|
||||
}
|
||||
|
||||
const updateSSH = async ({ res, request }) => {
|
||||
let { body: { id, name, authType, password, privateKey, date, tempKey } } = request
|
||||
let record = { name, authType, password, privateKey, date }
|
||||
if(!id || !name) return res.fail({ data: false, msg: '请输入凭据名称' })
|
||||
let sshRecord = await readSSHRecord()
|
||||
let idx = sshRecord.findIndex(item => item._id === id)
|
||||
if (sshRecord.some(item => item.name === name && item.date !== date)) return res.fail({ data: false, msg: '已存在同名凭证' })
|
||||
if(idx === -1) res.fail({ data: false, msg: '请输入凭据名称' })
|
||||
const oldRecord = sshRecord[idx]
|
||||
// 判断原记录是否存在当前更新记录的认证方式
|
||||
if (!oldRecord[authType] && !record[authType]) return res.fail({ data: false, msg: `请输入${ authType === 'password' ? '密码' : '密钥' }` })
|
||||
if (!record[authType] && oldRecord[authType]) {
|
||||
record[authType] = oldRecord[authType]
|
||||
} else {
|
||||
const clearTempKey = await RSADecryptSync(tempKey)
|
||||
console.log('clearTempKey:', clearTempKey)
|
||||
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||
}
|
||||
sshRecord.splice(idx, 1, record)
|
||||
await writeSSHRecord(sshRecord)
|
||||
consola.info('修改凭证:', name)
|
||||
res.success({ data: '保存成功' })
|
||||
}
|
||||
|
||||
const removeSSH = async ({ res, request }) => {
|
||||
let { body: { host } } = request
|
||||
let { params: { id } } = request
|
||||
let sshRecord = await readSSHRecord()
|
||||
let idx = sshRecord.findIndex(item => item.host === host)
|
||||
let idx = sshRecord.findIndex(item => item._id === id)
|
||||
if(idx === -1) return res.fail({ msg: '凭证不存在' })
|
||||
sshRecord.splice(idx, 1)
|
||||
consola.info('移除凭证:', host)
|
||||
// 将删除的凭证id从host中删除
|
||||
let hostList = await readHostList()
|
||||
hostList = hostList.map(item => {
|
||||
if (item.credential === id) item.credential = ''
|
||||
return item
|
||||
})
|
||||
await writeHostList(hostList)
|
||||
consola.info('移除凭证:', id)
|
||||
await writeSSHRecord(sshRecord)
|
||||
res.success({ data: '移除成功' })
|
||||
}
|
||||
|
||||
const existSSH = async ({ res, request }) => {
|
||||
let { body: { host } } = request
|
||||
let sshRecord = await readSSHRecord()
|
||||
let isExist = sshRecord?.some(item => item.host === host)
|
||||
consola.info('查询凭证:', host)
|
||||
if(!isExist) return res.success({ data: false }) // host不存在
|
||||
res.success({ data: true }) // 存在
|
||||
}
|
||||
|
||||
const getCommand = async ({ res, request }) => {
|
||||
let { host } = request.query
|
||||
if(!host) return res.fail({ data: false, msg: '参数错误' })
|
||||
@ -61,8 +90,8 @@ const getCommand = async ({ res, request }) => {
|
||||
|
||||
module.exports = {
|
||||
getSSHList,
|
||||
addSSH,
|
||||
updateSSH,
|
||||
removeSSH,
|
||||
existSSH,
|
||||
getCommand
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
const { getSSHList, updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh')
|
||||
const { getHostList, saveHost, updateHost, removeHost } = require('../controller/host')
|
||||
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh')
|
||||
const { getHostList, addHost, updateHost, removeHost } = require('../controller/host')
|
||||
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
||||
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
||||
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
||||
@ -10,21 +10,21 @@ const ssh = [
|
||||
path: '/get-ssh-list',
|
||||
controller: getSSHList
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/add-ssh',
|
||||
controller: addSSH
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/update-ssh',
|
||||
controller: updateSSH
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/remove-ssh',
|
||||
method: 'delete',
|
||||
path: '/remove-ssh/:id',
|
||||
controller: removeSSH
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/exist-ssh',
|
||||
controller: existSSH
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/command',
|
||||
@ -40,7 +40,7 @@ const host = [
|
||||
{
|
||||
method: 'post',
|
||||
path: '/host-save',
|
||||
controller: saveHost
|
||||
controller: addHost
|
||||
},
|
||||
{
|
||||
method: 'put',
|
||||
|
@ -52,6 +52,7 @@ module.exports = (httpServer) => {
|
||||
const sshRecord = await readSSHRecord()
|
||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
||||
if (!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
||||
// :TODO: 不用tempKey加密了,统一使用commonKey加密
|
||||
let { type, host, port, username, randomKey } = loginInfo
|
||||
try {
|
||||
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
||||
|
@ -10,22 +10,25 @@ export default {
|
||||
getSSHList(params = {}) {
|
||||
return axios({ url: '/get-ssh-list', method: 'get', params })
|
||||
},
|
||||
addSSH(data) {
|
||||
return axios({ url: '/add-ssh', method: 'post', data })
|
||||
},
|
||||
updateSSH(data) {
|
||||
return axios({ url: '/update-ssh', method: 'post', data })
|
||||
},
|
||||
removeSSH(host) {
|
||||
return axios({ url: '/remove-ssh', method: 'post', data: { host } })
|
||||
},
|
||||
existSSH(host) {
|
||||
return axios({ url: '/exist-ssh', method: 'post', data: { host } })
|
||||
removeSSH(id) {
|
||||
return axios({ url: `/remove-ssh/${ id }`, method: 'delete' })
|
||||
},
|
||||
// existSSH(host) {
|
||||
// return axios({ url: '/exist-ssh', method: 'post', data: { host } })
|
||||
// },
|
||||
getCommand(host) {
|
||||
return axios({ url: '/command', method: 'get', params: { host } })
|
||||
},
|
||||
getHostList() {
|
||||
return axios({ url: '/host-list', method: 'get' })
|
||||
},
|
||||
saveHost(data) {
|
||||
addHost(data) {
|
||||
return axios({ url: '/host-save', method: 'post', data })
|
||||
},
|
||||
updateHost(data) {
|
||||
|
@ -52,11 +52,11 @@ let menuList = reactive([
|
||||
icon: markRaw(ScaleToOriginal),
|
||||
index: '/terminal'
|
||||
},
|
||||
// {
|
||||
// name: '凭据管理',
|
||||
// icon: markRaw(Key),
|
||||
// index: '/credentials'
|
||||
// },
|
||||
{
|
||||
name: '凭据管理',
|
||||
icon: markRaw(Key),
|
||||
index: '/credentials'
|
||||
},
|
||||
{
|
||||
name: '分组管理',
|
||||
icon: markRaw(FolderOpened),
|
||||
|
@ -49,6 +49,11 @@ const useStore = defineStore({
|
||||
// console.log('groupList:', groupList)
|
||||
this.$patch({ groupList })
|
||||
},
|
||||
async getSSHList() {
|
||||
const { data: sshList } = await $api.getSSHList()
|
||||
// console.log('sshList:', sshList)
|
||||
this.$patch({ sshList })
|
||||
},
|
||||
getHostPing() {
|
||||
setTimeout(() => {
|
||||
this.hostList.forEach((item) => {
|
||||
|
@ -14,21 +14,11 @@ export const randomStr = (e) =>{
|
||||
// rsa公钥加密
|
||||
export const RSAEncrypt = (text) => {
|
||||
const publicKey = localStorage.getItem('publicKey')
|
||||
if(!publicKey) return -1 // 公钥不存在
|
||||
const RSAPubEncrypt = new JSRsaEncrypt() // 生成实例
|
||||
RSAPubEncrypt.setPublicKey(publicKey) // 配置公钥(不是将公钥实例化时传入!!!)
|
||||
const ciphertext = RSAPubEncrypt.encrypt(text) // 加密
|
||||
// console.log('rsa加密:', ciphertext)
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
// rsa公钥解密
|
||||
export const RSADecrypt = (text) => {
|
||||
const publicKey = localStorage.getItem('publicKey')
|
||||
if(!publicKey) return -1 // 公钥不存在
|
||||
if (!publicKey) return -1 // 公钥不存在
|
||||
const RSAPubEncrypt = new JSRsaEncrypt() // 生成实例
|
||||
RSAPubEncrypt.setPublicKey(publicKey) // 配置公钥(不是将公钥实例化时传入!!!)
|
||||
const ciphertext = RSAPubEncrypt.encrypt(text) // 加密
|
||||
// console.log('rsa公钥加密:', ciphertext)
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,204 @@
|
||||
<template>
|
||||
<div class="">
|
||||
credentials
|
||||
<div class="credentials_container">
|
||||
<div class="header">
|
||||
<el-button type="primary" @click="addCredentials">添加凭证</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="sshList">
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="authType" label="类型">
|
||||
<template #default="{ row }">
|
||||
{{ row.authType === 'privateKey' ? '密钥' : '密码' }}
|
||||
</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="removeSSH(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog
|
||||
v-model="sshFormVisible"
|
||||
width="600px"
|
||||
top="150px"
|
||||
:title="isModify ? '修改凭证' : '添加凭证'"
|
||||
:close-on-click-modal="false"
|
||||
@close="clearFormInfo"
|
||||
>
|
||||
<el-form
|
||||
ref="updateFormRef"
|
||||
:model="sshForm"
|
||||
:rules="rules"
|
||||
:hide-required-asterisk="true"
|
||||
label-suffix=":"
|
||||
label-width="100px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="凭证名称" prop="name">
|
||||
<el-input
|
||||
v-model.trim="sshForm.name"
|
||||
clearable
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="认证方式" prop="type">
|
||||
<el-radio v-model.trim="sshForm.authType" value="privateKey">密钥</el-radio>
|
||||
<el-radio v-model.trim="sshForm.authType" value="password">密码</el-radio>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="sshForm.authType === '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 v-if="sshForm.authType === 'password'" prop="password" label="密码">
|
||||
<el-input
|
||||
v-model.trim="sshForm.password"
|
||||
type="text"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span>
|
||||
<el-button @click="sshFormVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="updateForm">{{ isModify ? '修改' : '新增' }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
<script setup>
|
||||
import { ref, reactive, computed, nextTick, getCurrentInstance } from 'vue'
|
||||
import { randomStr, AESEncrypt, RSAEncrypt } from '@utils/index.js'
|
||||
|
||||
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
|
||||
|
||||
const loading = ref(false)
|
||||
const sshFormVisible = ref(false)
|
||||
let isModify = ref(false)
|
||||
const sshForm = reactive({
|
||||
name: '',
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
authType: 'privateKey',
|
||||
privateKey: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: { required: true, message: '需输入凭证名称', trigger: 'change' },
|
||||
password: [{ required: !isModify.value && sshForm.authType === 'password', trigger: 'change' },],
|
||||
privateKey: [{ required: !isModify.value && sshForm.authType === 'privateKey', trigger: 'change' },]
|
||||
}
|
||||
})
|
||||
|
||||
const updateFormRef = ref(null)
|
||||
const privateKeyRef = ref(null)
|
||||
|
||||
let sshList = computed(() => $store.sshList)
|
||||
|
||||
let addCredentials = () => {
|
||||
sshForm.id = null
|
||||
sshFormVisible.value = true
|
||||
isModify.value = false
|
||||
}
|
||||
const handleChange = (row) => {
|
||||
Object.assign(sshForm, { ...row })
|
||||
sshFormVisible.value = true
|
||||
isModify.value = true
|
||||
}
|
||||
|
||||
const updateForm = () => {
|
||||
updateFormRef.value.validate()
|
||||
.then(async () => {
|
||||
let formData = { ...sshForm }
|
||||
let tempKey = randomStr(16)
|
||||
// 加密传输
|
||||
if (formData.password) formData.password = AESEncrypt(formData.password, tempKey)
|
||||
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, tempKey)
|
||||
formData.tempKey = RSAEncrypt(tempKey)
|
||||
// 加密传输
|
||||
if (isModify.value) {
|
||||
await $api.updateSSH(formData)
|
||||
} else {
|
||||
await $api.addSSH(formData)
|
||||
}
|
||||
sshFormVisible.value = false
|
||||
await $store.getSSHList()
|
||||
$message.success('success')
|
||||
})
|
||||
}
|
||||
|
||||
const clearFormInfo = () => {
|
||||
nextTick(() => updateFormRef.value.resetFields())
|
||||
}
|
||||
|
||||
const removeSSH = ({ id, name }) => {
|
||||
$messageBox.confirm(`确认删除该凭证:${ name }`, 'Warning', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
await $api.removeSSH(id) // 后台会同步删除关联此凭证的credential字段
|
||||
await $store.getSSHList()
|
||||
await $store.getHostList() // 刷新主机字段 isConfig
|
||||
$message.success('success')
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.credentials_container {
|
||||
padding: 20px;
|
||||
.header {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
.host_count {
|
||||
display: block;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
color: #87cf63;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
@ -46,6 +46,7 @@ onBeforeMount(async () => {
|
||||
.router_box {
|
||||
min-height: calc(100vh - 60px - 20px);
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,6 @@ const { proxy: { $store, $api, $message, $router } } = getCurrentInstance()
|
||||
|
||||
const loginFormRefs = ref(null)
|
||||
const isSession = ref(true)
|
||||
const visible = ref(true)
|
||||
const notKey = ref(false)
|
||||
const loading = ref(false)
|
||||
const loginForm = reactive({
|
||||
|
@ -164,12 +164,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
import SSHForm from './ssh-form.vue'
|
||||
|
||||
const { proxy: { $api, $tools } } = getCurrentInstance()
|
||||
const { proxy: { $api, $router, $tools } } = getCurrentInstance()
|
||||
|
||||
const props = defineProps({
|
||||
hostInfo: {
|
||||
@ -226,22 +224,16 @@ const handleToConsole = () => {
|
||||
}
|
||||
|
||||
const handleSSH = async () => {
|
||||
let { data } = host.value
|
||||
}
|
||||
|
||||
const handleRemoveSSH = async () => {
|
||||
ElMessageBox.confirm('确认删除SSH凭证', 'Warning', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
let { data } = await $api.removeSSH(host.value)
|
||||
if(!hostInfo.value?.isConfig) {
|
||||
ElMessage({
|
||||
message: data,
|
||||
type: 'success',
|
||||
message: '请先配置SSH连接信息',
|
||||
type: 'warning',
|
||||
center: true
|
||||
})
|
||||
})
|
||||
handleUpdate()
|
||||
return
|
||||
}
|
||||
$router.push({ path: '/terminal', query: { host: host.value } })
|
||||
}
|
||||
|
||||
const handleRemoveHost = async () => {
|
||||
|
@ -2,6 +2,9 @@
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
top="45px"
|
||||
modal-class="host_form_dialog"
|
||||
append-to-body
|
||||
:title="title"
|
||||
:close-on-click-modal="false"
|
||||
@open="setDefaultData"
|
||||
@ -46,8 +49,8 @@
|
||||
<div key="instance_info" class="instance_info">
|
||||
<el-form-item
|
||||
key="host"
|
||||
class="form_item"
|
||||
label="实例"
|
||||
class="form_item_host"
|
||||
label="主机"
|
||||
prop="host"
|
||||
>
|
||||
<el-input
|
||||
@ -59,7 +62,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
key="port"
|
||||
class="form_item"
|
||||
class="form_item_port"
|
||||
label="端口"
|
||||
prop="port"
|
||||
>
|
||||
@ -86,6 +89,7 @@
|
||||
<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-radio v-model.trim="hostForm.authType" value="credential">凭据</el-radio>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="hostForm.authType === 'privateKey'"
|
||||
@ -131,6 +135,29 @@
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="hostForm.authType === 'credential'"
|
||||
key="credential"
|
||||
prop="credential"
|
||||
label="凭据"
|
||||
>
|
||||
<el-select v-model="hostForm.credential" class="credential_select" placeholder="">
|
||||
<template #empty>
|
||||
<div class="empty_credential">
|
||||
<span>无凭据数据,</span>
|
||||
<el-button type="primary" link @click="toCredentials">
|
||||
去添加
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in sshList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item key="command" prop="command" label="执行指令">
|
||||
<el-input
|
||||
v-model="hostForm.command"
|
||||
@ -204,8 +231,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, getCurrentInstance, nextTick } from 'vue'
|
||||
import { RSAEncrypt, AESEncrypt, randomStr } from '@utils/index.js'
|
||||
|
||||
const { proxy: { $api, $message, $store } } = getCurrentInstance()
|
||||
const { proxy: { $api, $router, $message, $store } } = getCurrentInstance()
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@ -229,6 +257,7 @@ const resetForm = () => ({
|
||||
authType: 'privateKey',
|
||||
password: '',
|
||||
privateKey: '',
|
||||
credential: '', // credentials -> _id
|
||||
index: 0,
|
||||
expired: null,
|
||||
expiredNotify: false,
|
||||
@ -265,7 +294,8 @@ const visible = computed({
|
||||
|
||||
const title = computed(() => props.defaultData ? '修改实例' : '新增实例')
|
||||
|
||||
let groupList = computed(() => $store.groupList || [])
|
||||
let groupList = computed(() => $store.groupList)
|
||||
let sshList = computed(() => $store.sshList)
|
||||
|
||||
const handleClosed = () => {
|
||||
// console.log('handleClosed')
|
||||
@ -312,14 +342,25 @@ const userSearch = (keyword, cb) => {
|
||||
cb(res)
|
||||
}
|
||||
|
||||
const toCredentials = () => {
|
||||
visible.value = false
|
||||
$router.push({ path: '/credentials' })
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
formRef.value.validate()
|
||||
.then(async () => {
|
||||
let tempKey = randomStr(16)
|
||||
let formData = { ...hostForm }
|
||||
// 加密传输
|
||||
if (formData.password) formData.password = AESEncrypt(formData.password, tempKey)
|
||||
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, tempKey)
|
||||
formData.tempKey = RSAEncrypt(tempKey)
|
||||
if (props.defaultData) {
|
||||
let { msg } = await $api.updateHost(Object.assign({}, hostForm, { oldHost: oldHost.value }))
|
||||
let { msg } = await $api.updateHost(Object.assign({}, formData, { oldHost: oldHost.value }))
|
||||
$message({ type: 'success', center: true, message: msg })
|
||||
} else {
|
||||
let { msg } = await $api.saveHost(hostForm)
|
||||
let { msg } = await $api.addHost(formData)
|
||||
$message({ type: 'success', center: true, message: msg })
|
||||
}
|
||||
visible.value = false
|
||||
@ -332,9 +373,18 @@ const handleSave = () => {
|
||||
<style lang="scss" scoped>
|
||||
.instance_info {
|
||||
display: flex;
|
||||
.form_item {
|
||||
width: 50%;
|
||||
justify-content: space-between;
|
||||
.form_item_host {
|
||||
width: 60%;
|
||||
}
|
||||
.form_item_port {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.empty_credential {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
|
@ -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>
|
@ -106,7 +106,7 @@ const unwatchHost = watch(hostList, () => {
|
||||
|
||||
const connectIo = () => {
|
||||
if (socket.value) socket.value.close()
|
||||
unwatchHost()
|
||||
if (typeof(unwatchHost) === 'function') unwatchHost()
|
||||
let socketInstance = io($serviceURI, {
|
||||
path: '/clients',
|
||||
forceNew: true,
|
||||
|
@ -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,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>
|
@ -2,7 +2,7 @@
|
||||
<div class="setting_container">
|
||||
<el-tabs tab-position="top">
|
||||
<el-tab-pane label="修改密码" lazy>
|
||||
<Password />
|
||||
<User />
|
||||
</el-tab-pane>
|
||||
<!-- <el-tab-pane label="分组管理">
|
||||
<Group />
|
||||
@ -24,31 +24,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import NotifyList from './components/notify-list.vue'
|
||||
import EmailList from './components/email-list.vue'
|
||||
// import Sort from './components/sort.vue'
|
||||
import Record from './components/record.vue'
|
||||
// import Group from './components/group.vue'
|
||||
import Password from './components/password.vue'
|
||||
import User from './components/user.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>
|
||||
|
@ -51,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onBeforeMount, getCurrentInstance } from 'vue'
|
||||
import { ref, reactive, computed, onBeforeMount,defineProps, getCurrentInstance } from 'vue'
|
||||
import TerminalTab from './terminal-tab.vue'
|
||||
import InfoSide from './info-side.vue'
|
||||
import SftpFooter from './sftp-footer.vue'
|
||||
@ -59,6 +59,13 @@ import InputCommand from '@/components/input-command/index.vue'
|
||||
|
||||
const { proxy: { $store, $router, $route, $nextTick } } = getCurrentInstance()
|
||||
|
||||
const props = defineProps({
|
||||
ternimalTabs: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const name = ref('')
|
||||
const host = ref('')
|
||||
const activeTab = ref('')
|
||||
@ -71,6 +78,7 @@ const visible = ref(true)
|
||||
const infoSideRef = ref(null)
|
||||
const terminalTabRefs = ref([])
|
||||
const token = computed(() => $store.token)
|
||||
const ternimalTabs = computed(() => props.ternimalTabs)
|
||||
|
||||
const closable = computed(() => terminalTabs.length > 1)
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div v-if="showLinkTips" class="terminal_link_tips">
|
||||
<h2 class="quick_link_text">快速连接</h2>
|
||||
<el-table
|
||||
:data="tabelData"
|
||||
:data="hostList"
|
||||
:show-header="false"
|
||||
>
|
||||
<el-table-column prop="name" label="name" />
|
||||
@ -16,7 +16,7 @@
|
||||
<template #default="{ row }">
|
||||
<div class="actios_btns">
|
||||
<el-button
|
||||
v-if="row.username && row.port"
|
||||
v-if="row.isConfig"
|
||||
type="primary"
|
||||
link
|
||||
@click="linkTerminal(row)"
|
||||
@ -27,7 +27,7 @@
|
||||
v-else
|
||||
type="success"
|
||||
link
|
||||
@click="confSSH(row)"
|
||||
@click="handleUpdateHost(row)"
|
||||
>
|
||||
配置ssh
|
||||
</el-button>
|
||||
@ -37,46 +37,59 @@
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Terminal />
|
||||
<Terminal :ternimal-tabs="ternimalTabs" />
|
||||
</div>
|
||||
<HostForm
|
||||
v-model:show="hostFormVisible"
|
||||
:default-data="updateHostData"
|
||||
@update-list="handleUpdateList"
|
||||
@closed="updateHostData = null"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import { ref, computed, onActivated, getCurrentInstance } from 'vue'
|
||||
import Terminal from './components/terminal.vue'
|
||||
import HostForm from '../server/components/host-form.vue'
|
||||
|
||||
const { proxy: { $store } } = getCurrentInstance()
|
||||
const { proxy: { $store, $message } } = getCurrentInstance()
|
||||
|
||||
let showLinkTips = ref(true)
|
||||
let ternimalTabs = ref([])
|
||||
const hostFormVisible = ref(false)
|
||||
const updateHostData = ref(null)
|
||||
|
||||
let showLinkTips = computed(() => !Boolean(ternimalTabs.value.length))
|
||||
|
||||
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)
|
||||
return hostList.value?.every(item => item.isConfig)
|
||||
})
|
||||
|
||||
function linkTerminal(row) {
|
||||
// console.log(row)
|
||||
ternimalTabs.value.push(row)
|
||||
showLinkTips.value = false
|
||||
}
|
||||
|
||||
function confSSH(row) {
|
||||
|
||||
function handleUpdateHost(row) {
|
||||
hostFormVisible.value = true
|
||||
updateHostData.value = { ...row }
|
||||
}
|
||||
|
||||
const handleUpdateList = async () => {
|
||||
try {
|
||||
await $store.getHostList()
|
||||
} catch (err) {
|
||||
$message.error('获取实例列表失败')
|
||||
console.error('获取实例列表失败: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
console.log()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
Loading…
x
Reference in New Issue
Block a user