✨ 支持批量修改与删除&调整实例面板UI
This commit is contained in:
parent
0eb83ad48b
commit
09e107b8d9
14
README.md
14
README.md
@ -6,13 +6,13 @@
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> 强烈建议使用 **iptables** 或 **fail2ban** 等安全服务限制IP访问,谨慎暴露面板服务到公网。
|
> 强烈建议使用 **iptables** 或 **fail2ban** 等安全服务限制IP访问,谨慎暴露面板服务到公网。
|
||||||
|
|
||||||
> [!NOTE]
|
<!-- > [!NOTE]
|
||||||
> 客户端信息监控与webssh功能都将以`该服务器作为中转`。中国大陆连接建议使用香港、新加坡、日本、韩国等地区的低延迟服务器来安装服务端
|
> webssh与监控服务都将以`该服务器作为中转`。中国大陆连接建议使用香港、新加坡、日本、韩国等地区的低延迟服务器来安装服务端 -->
|
||||||
|
|
||||||
<!-- - [功能](#功能)
|
<!-- - [功能](#功能)
|
||||||
- [安装](#安装指南)
|
- [安装](#安装指南)
|
||||||
- [服务端安装](#服务端安装)
|
- [服务端安装](#服务端安装)
|
||||||
- [客户端安装](#客户端安装)
|
- [监控服务安装](#监控服务安装)
|
||||||
- [版本日志](#版本日志)
|
- [版本日志](#版本日志)
|
||||||
- [安全与说明](#安全与说明)
|
- [安全与说明](#安全与说明)
|
||||||
- [开发](#开发)
|
- [开发](#开发)
|
||||||
@ -73,9 +73,9 @@ pm2 start index.js --name easynode-server
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 客户端安装
|
### 监控服务安装
|
||||||
|
|
||||||
- 客户端用于实时向服务端推送**系统、公网IP、CPU、内存、硬盘、网卡**等基础信息,不安装不影响使用面板,但是无法实时同步基础信息。
|
- 监控服务用于实时向服务端推送**系统、公网IP、CPU、内存、硬盘、网卡**等基础信息,不安装不影响使用面板,但是无法实时同步cpu占用、实时网速、硬盘容量等有用信息。
|
||||||
|
|
||||||
- 占用端口:**22022**
|
- 占用端口:**22022**
|
||||||
|
|
||||||
@ -91,9 +91,9 @@ curl -o- https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/
|
|||||||
curl -o- https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash
|
curl -o- https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
> 查看客户端状态:`systemctl status easynode-client`
|
> 查看监控服务状态:`systemctl status easynode-client`
|
||||||
>
|
>
|
||||||
> 查看客户端日志: `journalctl --follow -u easynode-client`
|
> 查看监控服务日志: `journalctl --follow -u easynode-client`
|
||||||
>
|
>
|
||||||
> 查看详细日志:journalctl -xe
|
> 查看详细日志:journalctl -xe
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "easynode客户端安装",
|
"name": "easynode监控服务安装",
|
||||||
"command": "curl -o- https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash",
|
"command": "curl -o- https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash",
|
||||||
"description": "easynode-客户端-安装脚本"
|
"description": "easynode-监控服务-安装脚本"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "easynode客户端卸载",
|
"name": "easynode监控服务卸载",
|
||||||
"command": "curl -o- https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash",
|
"command": "curl -o- https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash",
|
||||||
"description": "easynode-客户端-卸载脚本"
|
"description": "easynode-监控服务-卸载脚本"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "查询本机公网IP",
|
"name": "查询本机公网IP",
|
||||||
|
@ -6,7 +6,7 @@ async function getHostList({ res }) {
|
|||||||
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
let { username, port, authType, _id: id, credential } = item
|
let { username, port, authType, _id: id, credential } = item
|
||||||
// console.log('解密凭证title: ', credential)
|
console.log('解密凭证title: ', credential)
|
||||||
if (credential) credential = await AESDecryptSync(credential)
|
if (credential) credential = await AESDecryptSync(credential)
|
||||||
const isConfig = Boolean(username && port && (item[authType]))
|
const isConfig = Boolean(username && port && (item[authType]))
|
||||||
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
||||||
@ -34,7 +34,7 @@ async function addHost({
|
|||||||
const clearTempKey = await RSADecryptSync(tempKey)
|
const clearTempKey = await RSADecryptSync(tempKey)
|
||||||
console.log('clearTempKey:', clearTempKey)
|
console.log('clearTempKey:', clearTempKey)
|
||||||
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||||
// console.log(`${ authType }原密文: `, clearSSHKey)
|
console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
record[authType] = await AESEncryptSync(clearSSHKey)
|
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||||
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||||
hostList.push(record)
|
hostList.push(record)
|
||||||
@ -42,15 +42,44 @@ async function addHost({
|
|||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateHost({
|
async function updateHost({ res, request }) {
|
||||||
res, request
|
|
||||||
}) {
|
|
||||||
let {
|
let {
|
||||||
body: {
|
body: {
|
||||||
|
hosts,
|
||||||
host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark,
|
host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
port, username, authType, password, privateKey, credential, command, tempKey
|
port, username, authType, password, privateKey, credential, command, tempKey
|
||||||
}
|
}
|
||||||
} = request
|
} = request
|
||||||
|
let isBatch = Array.isArray(hosts)
|
||||||
|
console.log('isBatch:', isBatch)
|
||||||
|
if (isBatch) {
|
||||||
|
if (!hosts.length) return res.fail({ msg: 'hosts为空' })
|
||||||
|
let hostList = await readHostList()
|
||||||
|
console.log('批量修改: ', isBatch)
|
||||||
|
let newHostList = []
|
||||||
|
for (let oldRecord of hostList) {
|
||||||
|
let record = hosts.find(item => item.host === oldRecord.host)
|
||||||
|
if (!record) {
|
||||||
|
newHostList.push(oldRecord)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let { authType } = record
|
||||||
|
// 如果存在原认证方式则保存下来
|
||||||
|
if (!record[authType] && oldRecord[authType]) {
|
||||||
|
record[authType] = oldRecord[authType]
|
||||||
|
} else {
|
||||||
|
const clearTempKey = await RSADecryptSync(record.tempKey)
|
||||||
|
// console.log('批量解密tempKey:', clearTempKey)
|
||||||
|
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||||
|
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
|
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||||
|
// console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||||
|
}
|
||||||
|
newHostList.push(Object.assign(oldRecord, record))
|
||||||
|
}
|
||||||
|
await writeHostList(newHostList)
|
||||||
|
return res.success({ msg: '批量修改成功' })
|
||||||
|
}
|
||||||
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||||
let hostList = await readHostList()
|
let hostList = await readHostList()
|
||||||
let record = {
|
let record = {
|
||||||
@ -66,11 +95,11 @@ async function updateHost({
|
|||||||
record[authType] = oldRecord[authType]
|
record[authType] = oldRecord[authType]
|
||||||
} else {
|
} else {
|
||||||
const clearTempKey = await RSADecryptSync(tempKey)
|
const clearTempKey = await RSADecryptSync(tempKey)
|
||||||
console.log('clearTempKey:', clearTempKey)
|
// console.log('clearTempKey:', clearTempKey)
|
||||||
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||||
// console.log(`${ authType }原密文: `, clearSSHKey)
|
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
record[authType] = await AESEncryptSync(clearSSHKey)
|
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||||
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
// console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||||
}
|
}
|
||||||
hostList.splice(idx, 1, record)
|
hostList.splice(idx, 1, record)
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
@ -82,9 +111,14 @@ async function removeHost({
|
|||||||
}) {
|
}) {
|
||||||
let { body: { host } } = request
|
let { body: { host } } = request
|
||||||
let hostList = await readHostList()
|
let hostList = await readHostList()
|
||||||
let hostIdx = hostList.findIndex(item => item.host === host)
|
if (Array.isArray(host)) {
|
||||||
if (hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
|
hostList = hostList.filter(item => !host.includes(item.host))
|
||||||
hostList.splice(hostIdx, 1)
|
// if (hostList.length === 0) return res.fail({ msg: '没有可删除的实例' })
|
||||||
|
} else {
|
||||||
|
let hostIdx = hostList.findIndex(item => item.host === host)
|
||||||
|
if (hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
|
||||||
|
hostList.splice(hostIdx, 1)
|
||||||
|
}
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
res.success({ data: `${ host }已移除` })
|
res.success({ data: `${ host }已移除` })
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ module.exports = {
|
|||||||
|
|
||||||
// js
|
// js
|
||||||
'no-async-promise-executor': 0,
|
'no-async-promise-executor': 0,
|
||||||
|
'comma-dangle': 0,
|
||||||
'import/no-extraneous-dependencies': 0,
|
'import/no-extraneous-dependencies': 0,
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
@ -4,12 +4,23 @@
|
|||||||
width="600px"
|
width="600px"
|
||||||
top="45px"
|
top="45px"
|
||||||
modal-class="host_form_dialog"
|
modal-class="host_form_dialog"
|
||||||
append-to-body
|
:append-to-body="false"
|
||||||
:title="title"
|
:title="title"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@open="setDefaultData"
|
@open="handleOpen"
|
||||||
@closed="handleClosed"
|
@closed="handleClosed"
|
||||||
>
|
>
|
||||||
|
<div v-if="isBatchModify" class="batch_info">
|
||||||
|
<el-alert title="正在进行批量修改操作,留空默认保留原值" type="warning" :closable="false" />
|
||||||
|
<!-- <el-tag
|
||||||
|
v-for="item in batchHosts"
|
||||||
|
:key="item.id"
|
||||||
|
class="host_name_tag"
|
||||||
|
type="warning"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</el-tag> -->
|
||||||
|
</div>
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="hostForm"
|
:model="hostForm"
|
||||||
@ -21,7 +32,12 @@
|
|||||||
>
|
>
|
||||||
<transition-group name="list" mode="out-in" tag="div">
|
<transition-group name="list" mode="out-in" tag="div">
|
||||||
<el-form-item key="group" label="分组" prop="group">
|
<el-form-item key="group" label="分组" prop="group">
|
||||||
<el-select v-model="hostForm.group" placeholder="实例分组" style="width: 100%;">
|
<el-select
|
||||||
|
v-model="hostForm.group"
|
||||||
|
placeholder=""
|
||||||
|
clearable
|
||||||
|
style="width: 100%;"
|
||||||
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in groupList"
|
v-for="item in groupList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@ -30,7 +46,12 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="name" label="名称" prop="name">
|
<el-form-item
|
||||||
|
v-if="!isBatchModify"
|
||||||
|
key="name"
|
||||||
|
label="名称"
|
||||||
|
prop="name"
|
||||||
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="hostForm.name"
|
v-model="hostForm.name"
|
||||||
clearable
|
clearable
|
||||||
@ -40,6 +61,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div key="instance_info" class="instance_info">
|
<div key="instance_info" class="instance_info">
|
||||||
<el-form-item
|
<el-form-item
|
||||||
|
v-if="!isBatchModify"
|
||||||
key="host"
|
key="host"
|
||||||
class="form_item_host"
|
class="form_item_host"
|
||||||
label="主机"
|
label="主机"
|
||||||
@ -196,7 +218,12 @@
|
|||||||
@keyup.enter="handleSave"
|
@keyup.enter="handleSave"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="index" label="序号" prop="index">
|
<el-form-item
|
||||||
|
v-if="!isBatchModify"
|
||||||
|
key="index"
|
||||||
|
label="序号"
|
||||||
|
prop="index"
|
||||||
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim.number="hostForm.index"
|
v-model.trim.number="hostForm.index"
|
||||||
clearable
|
clearable
|
||||||
@ -240,11 +267,21 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
isBatchModify: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
batchHosts: {
|
||||||
|
required: false,
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:show', 'update-list', 'closed',])
|
const emit = defineEmits(['update:show', 'update-list', 'closed',])
|
||||||
|
|
||||||
const resetForm = () => ({
|
const formField = {
|
||||||
group: 'default',
|
group: 'default',
|
||||||
name: '',
|
name: '',
|
||||||
host: '',
|
host: '',
|
||||||
@ -260,18 +297,23 @@ const resetForm = () => ({
|
|||||||
consoleUrl: '',
|
consoleUrl: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
command: ''
|
command: ''
|
||||||
})
|
}
|
||||||
|
|
||||||
const hostForm = reactive(resetForm())
|
let hostForm = ref({ ...formField })
|
||||||
const privateKeyRef = ref(null)
|
let privateKeyRef = ref(null)
|
||||||
const oldHost = ref('')
|
let oldHost = ref('')
|
||||||
|
let formRef = ref(null)
|
||||||
|
|
||||||
|
let isBatchModify = computed(() => props.isBatchModify)
|
||||||
|
let batchHosts = computed(() => props.batchHosts)
|
||||||
|
let defaultData = computed(() => props.defaultData)
|
||||||
const rules = computed(() => {
|
const rules = computed(() => {
|
||||||
return {
|
return {
|
||||||
group: { required: true, message: '选择一个分组' },
|
group: { required: !isBatchModify.value, message: '选择一个分组' },
|
||||||
name: { required: true, message: '输入实例别名', trigger: 'change' },
|
name: { required: !isBatchModify.value, message: '输入实例别名', trigger: 'change' },
|
||||||
host: { required: true, message: '输入IP/域名', trigger: 'change' },
|
host: { required: !isBatchModify.value, message: '输入IP/域名', trigger: 'change' },
|
||||||
port: { required: true, type: 'number', message: '输入ssh端口', trigger: 'change' },
|
port: { required: true, type: 'number', message: '输入ssh端口', trigger: 'change' },
|
||||||
index: { required: true, type: 'number', message: '输入数字', trigger: 'change' },
|
index: { required: !isBatchModify.value, type: 'number', message: '输入数字', trigger: 'change' },
|
||||||
// password: [{ required: hostForm.authType === 'password', trigger: 'change' },],
|
// password: [{ required: hostForm.authType === 'password', trigger: 'change' },],
|
||||||
// privateKey: [{ required: hostForm.authType === 'privateKey', trigger: 'change' },],
|
// privateKey: [{ required: hostForm.authType === 'privateKey', trigger: 'change' },],
|
||||||
expired: { required: false },
|
expired: { required: false },
|
||||||
@ -281,30 +323,41 @@ const rules = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRef = ref(null)
|
|
||||||
|
|
||||||
const visible = computed({
|
const visible = computed({
|
||||||
get: () => props.show,
|
get: () => props.show,
|
||||||
set: (newVal) => emit('update:show', newVal)
|
set: (newVal) => emit('update:show', newVal)
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = computed(() => props.defaultData ? '修改实例' : '添加实例')
|
const title = computed(() => {
|
||||||
|
return isBatchModify.value ? '批量修改实例' : (defaultData.value ? '修改实例' : '添加实例')
|
||||||
|
})
|
||||||
|
|
||||||
let groupList = computed(() => $store.groupList)
|
let groupList = computed(() => $store.groupList)
|
||||||
let sshList = computed(() => $store.sshList)
|
let sshList = computed(() => $store.sshList)
|
||||||
|
|
||||||
const handleClosed = () => {
|
const setDefaultData = () => {
|
||||||
// console.log('handleClosed')
|
if (!defaultData.value) return
|
||||||
Object.assign(hostForm, resetForm())
|
let { host } = defaultData.value
|
||||||
emit('closed')
|
oldHost.value = host
|
||||||
nextTick(() => formRef.value.resetFields())
|
Object.assign(hostForm.value, { ...defaultData.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const setDefaultData = () => {
|
const setBatchDefaultData = () => {
|
||||||
if (!props.defaultData) return
|
if (!isBatchModify.value) return
|
||||||
let { host } = props.defaultData
|
Object.assign(hostForm.value, { ...formField }, { group: '' })
|
||||||
oldHost.value = host
|
}
|
||||||
Object.assign(hostForm, { ...props.defaultData })
|
const handleOpen = async () => {
|
||||||
|
setDefaultData()
|
||||||
|
setBatchDefaultData()
|
||||||
|
await nextTick()
|
||||||
|
formRef.value.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClosed = async () => {
|
||||||
|
emit('closed')
|
||||||
|
Object.assign(hostForm.value, { ...formField })
|
||||||
|
await nextTick()
|
||||||
|
formRef.value.resetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClickUploadBtn = () => {
|
const handleClickUploadBtn = () => {
|
||||||
@ -315,7 +368,7 @@ const handleSelectPrivateKeyFile = (event) => {
|
|||||||
let file = event.target.files[0]
|
let file = event.target.files[0]
|
||||||
let reader = new FileReader()
|
let reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
hostForm.privateKey = e.target.result
|
hostForm.value.privateKey = e.target.result
|
||||||
privateKeyRef.value = ''
|
privateKeyRef.value = ''
|
||||||
}
|
}
|
||||||
reader.readAsText(file)
|
reader.readAsText(file)
|
||||||
@ -346,30 +399,53 @@ const toCredentials = () => {
|
|||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
formRef.value.validate()
|
formRef.value.validate()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
let tempKey = randomStr(16)
|
let formData = { ...hostForm.value }
|
||||||
let formData = { ...hostForm }
|
if (isBatchModify.value) {
|
||||||
console.log('formData:', formData)
|
// eslint-disable-next-line
|
||||||
// 加密传输
|
let updateFileData = Object.fromEntries(Object.entries(formData).filter(([key, value]) => Boolean(value))) // 剔除掉未更改的值
|
||||||
if (formData.password) formData.password = AESEncrypt(formData.password, tempKey)
|
// console.log(updateFileData)
|
||||||
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, tempKey)
|
let newHosts = batchHosts.value
|
||||||
if (formData.credential) formData.credential = AESEncrypt(formData.credential, tempKey)
|
.map(item => ({ ...item, ...updateFileData }))
|
||||||
formData.tempKey = RSAEncrypt(tempKey)
|
.map(item => {
|
||||||
if (props.defaultData) {
|
const { authType } = item
|
||||||
let { msg } = await $api.updateHost(Object.assign({}, formData, { oldHost: oldHost.value }))
|
let tempKey = randomStr(16)
|
||||||
|
if (item[authType]) item[authType] = AESEncrypt(item[authType], tempKey)
|
||||||
|
item.tempKey = RSAEncrypt(tempKey)
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
let { msg } = await $api.updateHost({ hosts: newHosts })
|
||||||
$message({ type: 'success', center: true, message: msg })
|
$message({ type: 'success', center: true, message: msg })
|
||||||
} else {
|
} else {
|
||||||
let { msg } = await $api.addHost(formData)
|
let tempKey = randomStr(16)
|
||||||
$message({ type: 'success', center: true, message: msg })
|
let { authType } = formData
|
||||||
|
if (formData[authType]) formData[authType] = AESEncrypt(formData[authType], tempKey)
|
||||||
|
formData.tempKey = RSAEncrypt(tempKey)
|
||||||
|
if (defaultData.value) {
|
||||||
|
let { msg } = await $api.updateHost(Object.assign({}, formData, { oldHost: oldHost.value }))
|
||||||
|
$message({ type: 'success', center: true, message: msg })
|
||||||
|
} else {
|
||||||
|
let { msg } = await $api.addHost(formData)
|
||||||
|
$message({ type: 'success', center: true, message: msg })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
visible.value = false
|
visible.value = false
|
||||||
const { host, username, port, authType } = formData
|
emit('update-list')
|
||||||
emit('update-list', { isConfig: Boolean(username && port && (formData[authType])), host })
|
|
||||||
Object.assign(hostForm, resetForm())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.batch_info {
|
||||||
|
:deep(.el-alert) {
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
:deep(.el-tag) {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.instance_info {
|
.instance_info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
273
web/src/views/server/components/host-table.vue
Normal file
273
web/src/views/server/components/host-table.vue
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<el-card shadow="always" class="host_card">
|
||||||
|
<el-table ref="multipleTableRef" :data="tableData" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" />
|
||||||
|
<el-table-column prop="index" label="序号" width="100px" />
|
||||||
|
<el-table-column label="名称">
|
||||||
|
<template #default="scope">{{ scope.row.name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column property="username" label="用户名" />
|
||||||
|
<el-table-column property="host" label="IP" />
|
||||||
|
<el-table-column property="port" label="端口" />
|
||||||
|
<!-- <el-table-column property="port" label="认证类型">
|
||||||
|
<template #default="scope">{{ scope.row.authType === 'password' ? '密码' : '密钥' }}</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
<el-table-column property="isConfig" label="监控服务">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.osInfo" type="success">已安装</el-tag>
|
||||||
|
<el-tag v-else type="warning">未安装</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- <el-table-column property="isConfig" label="登录配置" /> -->
|
||||||
|
<el-table-column label="操作" width="300px">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip
|
||||||
|
:disabled="row.isConfig"
|
||||||
|
effect="dark"
|
||||||
|
content="请先配置ssh连接信息"
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<!-- <el-button type="warning">连接终端</el-button> -->
|
||||||
|
<el-button type="success" :disabled="!row.isConfig" @click="handleSSH(row)">连接终端</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-button type="primary" @click="handleUpdate(row)">修改</el-button>
|
||||||
|
<el-button type="danger" @click="handleRemoveHost(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, getCurrentInstance, reactive, watch } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const { proxy: { $api, $router, $tools } } = getCurrentInstance()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
hosts: {
|
||||||
|
required: true,
|
||||||
|
type: Array
|
||||||
|
},
|
||||||
|
hiddenIp: {
|
||||||
|
required: true,
|
||||||
|
type: [Number, Boolean,]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update-list', 'update-host', 'select-change',])
|
||||||
|
|
||||||
|
let tableData = ref([])
|
||||||
|
|
||||||
|
watch(() => props.hosts, (newVal) => {
|
||||||
|
console.log('newVal:', newVal)
|
||||||
|
tableData.value = newVal?.map(item => {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
let { cpuInfo, memInfo, osInfo, driveInfo, ipInfo, netstatInfo, ...rest } = item
|
||||||
|
return rest
|
||||||
|
}) || []
|
||||||
|
}, { immediate: true, deep: false })
|
||||||
|
|
||||||
|
const hostInfo = computed(() => props.hostInfo || {})
|
||||||
|
// const host = computed(() => hostInfo.value?.host)
|
||||||
|
const name = computed(() => hostInfo.value?.name)
|
||||||
|
const ping = computed(() => hostInfo.value?.ping || '')
|
||||||
|
const expiredTime = computed(() => $tools.formatTimestamp(hostInfo.value?.expired, 'date'))
|
||||||
|
const consoleUrl = computed(() => hostInfo.value?.consoleUrl)
|
||||||
|
const ipInfo = computed(() => hostInfo.value?.ipInfo || {})
|
||||||
|
const isError = computed(() => !Boolean(hostInfo.value?.osInfo))
|
||||||
|
const cpuInfo = computed(() => hostInfo.value?.cpuInfo || {})
|
||||||
|
const memInfo = computed(() => hostInfo.value?.memInfo || {})
|
||||||
|
const osInfo = computed(() => hostInfo.value?.osInfo || {})
|
||||||
|
const driveInfo = computed(() => hostInfo.value?.driveInfo || {})
|
||||||
|
const netstatInfo = computed(() => {
|
||||||
|
let { total: netTotal, ...netCards } = hostInfo.value?.netstatInfo || {}
|
||||||
|
return { netTotal, netCards: netCards || {} }
|
||||||
|
})
|
||||||
|
const openedCount = computed(() => hostInfo.value?.openedCount || 0)
|
||||||
|
|
||||||
|
const setColor = (num) => {
|
||||||
|
num = Number(num)
|
||||||
|
return num ? (num < 80 ? '#595959' : (num >= 80 && num < 90 ? '#FF6600' : '#FF0000')) : '#595959'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdate = (hostInfo) => {
|
||||||
|
emit('update-host', hostInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToConsole = () => {
|
||||||
|
window.open(consoleUrl.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSSH = async ({ host }) => {
|
||||||
|
// if (!hostInfo?.isConfig) {
|
||||||
|
// ElMessage({
|
||||||
|
// message: '请先配置SSH连接信息',
|
||||||
|
// type: 'warning',
|
||||||
|
// center: true
|
||||||
|
// })
|
||||||
|
// handleUpdate()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
$router.push({ path: '/terminal', query: { host } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectionChange = (val) => {
|
||||||
|
// console.log(val)
|
||||||
|
// selectHosts.value = val
|
||||||
|
emit('select-change', val)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveHost = async ({ host }) => {
|
||||||
|
ElMessageBox.confirm('确认删除实例', 'Warning', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
let { data } = await $api.removeHost({ host })
|
||||||
|
ElMessage({
|
||||||
|
message: data,
|
||||||
|
type: 'success',
|
||||||
|
center: true
|
||||||
|
})
|
||||||
|
emit('update-list')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.host_card {
|
||||||
|
margin: -10px 30px 0 30px;
|
||||||
|
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: 10px;
|
||||||
|
// transform: rotate(-45deg);
|
||||||
|
// transform: scale(0.95);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
.actions-icon {
|
||||||
|
margin: 0 10px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #1989fa;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,10 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="server_group_container">
|
<div class="server_group_container">
|
||||||
<div class="server_group_header">
|
<div class="server_group_header">
|
||||||
|
<el-dropdown>
|
||||||
|
<el-button type="primary" class="group_action_btn">
|
||||||
|
批量操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="handleBatchModify">批量修改</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="handleBatchRemove">批量删除</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<!-- <el-button v-show="selectHosts.length" type="primary" @click="hostFormVisible = true">批量操作</el-button> -->
|
||||||
<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> -->
|
||||||
<el-button type="primary" @click="importVisible = true">导入实例</el-button>
|
<el-button type="primary" @click="importVisible = true">导入实例</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="server_group_collapse">
|
<div class="server_group_collapse">
|
||||||
@ -16,20 +28,27 @@
|
|||||||
</el-empty>
|
</el-empty>
|
||||||
</div>
|
</div>
|
||||||
<el-collapse v-else v-model="activeGroup">
|
<el-collapse v-else v-model="activeGroup">
|
||||||
<el-collapse-item v-for="(servers, groupName) in groupHostList" :key="groupName" :name="groupName">
|
<el-collapse-item v-for="(hosts, groupName) in groupHostList" :key="groupName" :name="groupName">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="group_title">
|
<div class="group_title">
|
||||||
{{ groupName }}
|
{{ groupName }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="host_card_container">
|
<div class="host_card_container">
|
||||||
<HostCard
|
<!-- <HostCard
|
||||||
v-for="(item, index) in servers"
|
v-for="(item, index) in hosts"
|
||||||
:key="index"
|
:key="index"
|
||||||
:host-info="item"
|
:host-info="item"
|
||||||
:hidden-ip="hiddenIp"
|
:hidden-ip="hiddenIp"
|
||||||
@update-host="handleUpdateHost"
|
@update-host="handleUpdateHost"
|
||||||
@update-list="handleUpdateList"
|
@update-list="handleUpdateList"
|
||||||
|
/> -->
|
||||||
|
<HostTable
|
||||||
|
:hosts="hosts"
|
||||||
|
:hidden-ip="hiddenIp"
|
||||||
|
@update-host="handleUpdateHost"
|
||||||
|
@update-list="handleUpdateList"
|
||||||
|
@select-change="handleSelectChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
@ -38,8 +57,10 @@
|
|||||||
<HostForm
|
<HostForm
|
||||||
v-model:show="hostFormVisible"
|
v-model:show="hostFormVisible"
|
||||||
:default-data="updateHostData"
|
:default-data="updateHostData"
|
||||||
|
:is-batch-modify="isBatchModify"
|
||||||
|
:batch-hosts="selectHosts"
|
||||||
@update-list="handleUpdateList"
|
@update-list="handleUpdateList"
|
||||||
@closed="updateHostData = null"
|
@closed="updateHostData = null;isBatchModify = false"
|
||||||
/>
|
/>
|
||||||
<ImportHost
|
<ImportHost
|
||||||
v-model:show="importVisible"
|
v-model:show="importVisible"
|
||||||
@ -49,16 +70,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, getCurrentInstance, computed, watch } from 'vue'
|
import { h, ref, getCurrentInstance, computed, watch } from 'vue'
|
||||||
import HostCard from './components/host-card.vue'
|
// import HostCard from './components/host-card.vue'
|
||||||
|
import HostTable from './components/host-table.vue'
|
||||||
import HostForm from './components/host-form.vue'
|
import HostForm from './components/host-form.vue'
|
||||||
import ImportHost from './components/import-host.vue'
|
import ImportHost from './components/import-host.vue'
|
||||||
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const { proxy: { $store, $message } } = getCurrentInstance()
|
const { proxy: { $api, $store, $message, $messageBox } } = getCurrentInstance()
|
||||||
|
|
||||||
let updateHostData = ref(null)
|
let updateHostData = ref(null)
|
||||||
let hostFormVisible = ref(false)
|
let hostFormVisible = ref(false)
|
||||||
let importVisible = ref(false)
|
let importVisible = ref(false)
|
||||||
|
let selectHosts = ref([])
|
||||||
|
let isBatchModify = ref(false)
|
||||||
|
|
||||||
let hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
let hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
||||||
let activeGroup = ref([])
|
let activeGroup = ref([])
|
||||||
@ -72,6 +97,33 @@ let handleUpdateList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let handleSelectChange = (val) => {
|
||||||
|
selectHosts.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
let handleBatchModify = async () => {
|
||||||
|
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||||
|
isBatchModify.value = true
|
||||||
|
hostFormVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let handleBatchRemove = async () => {
|
||||||
|
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||||
|
let ips = selectHosts.value.map(item => item.host)
|
||||||
|
let names = selectHosts.value.map(item => item.name)
|
||||||
|
|
||||||
|
$messageBox.confirm(() => h('p', { style: 'line-height: 18px;' }, `确认删除\n${ names.join(', ') }吗?`), 'Warning', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
let { data } = await $api.removeHost({ host: ips })
|
||||||
|
$message({ message: data, type: 'success', center: true })
|
||||||
|
selectHosts.value = []
|
||||||
|
await handleUpdateList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let handleUpdateHost = (defaultData) => {
|
let handleUpdateHost = (defaultData) => {
|
||||||
hostFormVisible.value = true
|
hostFormVisible.value = true
|
||||||
updateHostData.value = defaultData
|
updateHostData.value = defaultData
|
||||||
@ -82,14 +134,15 @@ let handleHiddenIP = () => {
|
|||||||
localStorage.setItem('hiddenIp', String(hiddenIp.value))
|
localStorage.setItem('hiddenIp', String(hiddenIp.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hostList = computed(() => $store.hostList)
|
||||||
|
|
||||||
let groupHostList = computed(() => {
|
let groupHostList = computed(() => {
|
||||||
let res = {}
|
let res = {}
|
||||||
let hostList = $store.hostList
|
|
||||||
let groupList = $store.groupList
|
let groupList = $store.groupList
|
||||||
groupList.forEach(group => {
|
groupList.forEach(group => {
|
||||||
res[group.name] = []
|
res[group.name] = []
|
||||||
})
|
})
|
||||||
hostList.forEach(item => {
|
hostList.value.forEach(item => {
|
||||||
const group = groupList.find(group => group.id === item.group)
|
const group = groupList.find(group => group.id === item.group)
|
||||||
if (group) {
|
if (group) {
|
||||||
res[group.name].push(item)
|
res[group.name].push(item)
|
||||||
@ -121,18 +174,24 @@ let isNoHost = computed(() => Object.keys(groupHostList.value).length === 0)
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
.group_action_btn {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.server_group_collapse {
|
.server_group_collapse {
|
||||||
|
:deep(.el-collapse-item__header) {
|
||||||
|
padding: 0 35px;
|
||||||
|
}
|
||||||
.group_title {
|
.group_title {
|
||||||
margin: 0 15px;
|
// margin: 0 15px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.host_card_container {
|
.host_card_container {
|
||||||
padding-top: 25px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
.or {
|
.or {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
|
@ -204,7 +204,7 @@ const handleUpdateList = async ({ isConfig, host }) => {
|
|||||||
|
|
||||||
const handleResizeTerminalSftp = () => {
|
const handleResizeTerminalSftp = () => {
|
||||||
$nextTick(() => {
|
$nextTick(() => {
|
||||||
mainHeight.value = document.querySelector('.terminals_sftp_wrap').offsetHeight - 45 // 45 is tab-header height+15
|
mainHeight.value = document.querySelector('.terminals_sftp_wrap')?.offsetHeight - 45 // 45 is tab-header height+15
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user