✨ 新增导出功能&服务器列表排序缓存
This commit is contained in:
parent
ba67533e9a
commit
d83795d7af
@ -5,11 +5,15 @@ async function getHostList({ res }) {
|
||||
let data = await readHostList()
|
||||
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
||||
for (const item of data) {
|
||||
let { username, port, authType, _id: id, credential } = item
|
||||
// console.log('解密凭证title: ', credential)
|
||||
if (credential) credential = await AESDecryptSync(credential)
|
||||
const isConfig = Boolean(username && port && (item[authType]))
|
||||
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
||||
try {
|
||||
let { username, port, authType, _id: id, credential } = item
|
||||
// console.log('解密凭证title: ', credential)
|
||||
if (credential) credential = await AESDecryptSync(credential)
|
||||
const isConfig = Boolean(username && port && (item[authType]))
|
||||
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
||||
} catch (error) {
|
||||
consola.error('getHostList error: ', error.message)
|
||||
}
|
||||
}
|
||||
res.success({ data })
|
||||
}
|
||||
@ -119,13 +123,13 @@ async function removeHost({
|
||||
hostList.splice(hostIdx, 1)
|
||||
}
|
||||
writeHostList(hostList)
|
||||
res.success({ data: `${ host }已移除` })
|
||||
res.success({ data: '已移除' })
|
||||
}
|
||||
|
||||
async function importHost({
|
||||
res, request
|
||||
}) {
|
||||
let { body: { importHost } } = request
|
||||
let { body: { importHost, isEasyNodeJson = false } } = request
|
||||
if (!Array.isArray(importHost)) return res.fail({ msg: '参数错误' })
|
||||
let hostList = await readHostList()
|
||||
// 过滤已存在的host
|
||||
@ -134,15 +138,26 @@ async function importHost({
|
||||
let newHostListLen = newHostList.length
|
||||
if (newHostListLen === 0) return res.fail({ msg: '导入的实例已存在' })
|
||||
|
||||
let extraFiels = {
|
||||
expired: null, expiredNotify: false, group: 'default', consoleUrl: '', remark: '',
|
||||
authType: 'privateKey', password: '', privateKey: '', credential: '', command: ''
|
||||
if (isEasyNodeJson) {
|
||||
newHostList = newHostList.map((item) => {
|
||||
item.credential = ''
|
||||
item.isConfig = false
|
||||
delete item.id
|
||||
delete item.isConfig
|
||||
return item
|
||||
})
|
||||
} else {
|
||||
let extraFiels = {
|
||||
expired: null, expiredNotify: false, group: 'default', consoleUrl: '', remark: '',
|
||||
authType: 'privateKey', password: '', privateKey: '', credential: '', command: ''
|
||||
}
|
||||
newHostList = newHostList.map((item, index) => {
|
||||
item.port = Number(item.port) || 0
|
||||
item.index = newHostListLen - index
|
||||
return Object.assign(item, { ...extraFiels })
|
||||
})
|
||||
|
||||
}
|
||||
newHostList = newHostList.map((item, index) => {
|
||||
item.port = Number(item.port) || 0
|
||||
item.index = newHostListLen - index
|
||||
return Object.assign(item, { ...extraFiels })
|
||||
})
|
||||
hostList.push(...newHostList)
|
||||
writeHostList(hostList)
|
||||
res.success({ data: { len: newHostList.length } })
|
||||
|
@ -15,7 +15,7 @@ async function getClientsInfo(clientSockets) {
|
||||
})
|
||||
hostList
|
||||
.map(({ host, name }) => {
|
||||
if (clientSockets.some(item => item.host === host)) return { name, isIo: true } // 已经建立io连接(无论是否连接成功)的host不再重复建立连接
|
||||
if (clientSockets.some(item => item.host === host)) return { name, isIo: true } // 已经建立io连接(无论是否连接成功)的host不再重复建立连接,因为存在多次(reconnectionAttempts)的重试机制
|
||||
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
||||
path: '/client/os-info',
|
||||
forceNew: true,
|
||||
@ -34,7 +34,7 @@ async function getClientsInfo(clientSockets) {
|
||||
.forEach((item) => {
|
||||
if (item.isIo) return // console.log('已经建立io连接的host不再重复建立连接', item.name)
|
||||
const { host, name, clientSocket } = item
|
||||
clientsData[host] = { connect: false }
|
||||
// clientsData[host] = { connect: false }
|
||||
clientSocket
|
||||
.on('connect', () => {
|
||||
consola.success('client connect success:', host, name)
|
||||
|
@ -17,7 +17,7 @@ export default {
|
||||
return `${ (netSpeedMB * 1024).toFixed(1) } KB/s`
|
||||
},
|
||||
// format: time OR date
|
||||
formatTimestamp: (timestamp, format = 'time') => {
|
||||
formatTimestamp: (timestamp, format = 'time', afterSeparator = ':') => {
|
||||
if(typeof(timestamp) !== 'number') return '--'
|
||||
let date = new Date(timestamp)
|
||||
let padZero = (num) => String(num).padStart(2, '0')
|
||||
@ -31,9 +31,9 @@ export default {
|
||||
case 'date':
|
||||
return `${ year }-${ mounth }-${ day }`
|
||||
case 'time':
|
||||
return `${ year }-${ mounth }-${ day } ${ hours }:${ minute }:${ second }`
|
||||
return `${ year }-${ mounth }-${ day } ${ hours }${ afterSeparator }${ minute }${ afterSeparator }${ second }`
|
||||
default:
|
||||
return `${ year }-${ mounth }-${ day } ${ hours }:${ minute }:${ second }`
|
||||
return `${ year }-${ mounth }-${ day } ${ hours }${ afterSeparator }${ minute }${ afterSeparator }${ second }`
|
||||
}
|
||||
},
|
||||
ping
|
||||
|
@ -114,3 +114,17 @@ export const downloadFile = ({ buffer, name }) => {
|
||||
export const getSuffix = (name = '') => {
|
||||
return String(name).split(/\./).pop()
|
||||
}
|
||||
|
||||
export const exportFile = (data, filename, mimeType = 'application/json') =>{
|
||||
const blob = new Blob([JSON.stringify(data),], { type: mimeType })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', filename)
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
ref="tableRef"
|
||||
:data="hosts"
|
||||
row-key="host"
|
||||
:default-sort="defaultSort"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="expand">
|
||||
@ -43,8 +45,18 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="selection" reserve-selection />
|
||||
<el-table-column prop="index" label="序号" width="100px" />
|
||||
<el-table-column label="名称">
|
||||
<el-table-column
|
||||
property="index"
|
||||
label="序号"
|
||||
sortable
|
||||
width="100px"
|
||||
/>
|
||||
<el-table-column
|
||||
label="名称"
|
||||
property="name"
|
||||
sortable
|
||||
:sort-method="(a, b) => a.name - b.name"
|
||||
>
|
||||
<template #default="scope">{{ scope.row.name }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="username" label="用户名" />
|
||||
@ -53,9 +65,15 @@
|
||||
<!-- <el-table-column property="port" label="认证类型">
|
||||
<template #default="scope">{{ scope.row.authType === 'password' ? '密码' : '密钥' }}</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="监控服务">
|
||||
<el-table-column
|
||||
label="监控服务"
|
||||
property="monitorData"
|
||||
sortable
|
||||
:sort-method="(a, b) => a.monitorData?.connect - b.monitorData?.connect"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.monitorData?.connect" type="success">已安装</el-tag>
|
||||
<el-tag v-if="typeof(scope.row.monitorData?.connect) !== 'boolean'" type="info">连接中</el-tag>
|
||||
<el-tag v-else-if="scope.row.monitorData?.connect" type="success">已安装</el-tag>
|
||||
<el-tag v-else type="warning">未安装</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -70,7 +88,7 @@
|
||||
>
|
||||
<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="primary" @click="handleUpdate(row)">配置</el-button>
|
||||
<el-button type="danger" @click="handleRemoveHost(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -127,6 +145,15 @@ const handleOnekey = async (row) => {
|
||||
$router.push({ path: '/onekey', query: { host, execClientInstallScript: 'true' } })
|
||||
}
|
||||
|
||||
let defaultSortLocal = localStorage.getItem('host_table_sort')
|
||||
defaultSortLocal = defaultSortLocal ? JSON.parse(defaultSortLocal) : { prop: 'index', order: 'ascending' }
|
||||
let defaultSort = ref(defaultSortLocal)
|
||||
|
||||
const handleSortChange = (sortObj) => {
|
||||
defaultSort.value = sortObj
|
||||
localStorage.setItem('host_table_sort', JSON.stringify(sortObj))
|
||||
}
|
||||
|
||||
let selectHosts = ref([])
|
||||
const handleSelectionChange = (val) => {
|
||||
// console.log('select: ', val)
|
||||
|
@ -24,7 +24,7 @@
|
||||
@change="handleCsvFile"
|
||||
>
|
||||
</li>
|
||||
<li @click="handleFromJson">
|
||||
<li @click="handleFromJson(false)">
|
||||
<svg-icon name="icon-json" class="icon" />
|
||||
<span class="from">FinalShell</span>
|
||||
<span class="type">(json)</span>
|
||||
@ -35,19 +35,24 @@
|
||||
multiple
|
||||
name="jsonInput"
|
||||
style="display: none;"
|
||||
@click.stop
|
||||
@change="handleJsonFile"
|
||||
>
|
||||
</li>
|
||||
<li @click="handleFromJson(true)">
|
||||
<svg-icon name="icon-json" class="icon" />
|
||||
<span class="from">EadyNode</span>
|
||||
<span class="type">(json)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, getCurrentInstance, nextTick } from 'vue'
|
||||
import { RSAEncrypt, AESEncrypt, randomStr } from '@utils/index.js'
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import { parse } from 'csv-parse/browser/esm/sync'
|
||||
|
||||
const { proxy: { $api, $router, $message, $store } } = getCurrentInstance()
|
||||
const { proxy: { $api, $message } } = getCurrentInstance()
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@ -69,7 +74,11 @@ function handleFromCsv() {
|
||||
csvInputRef.value.click()
|
||||
}
|
||||
|
||||
function handleFromJson() {
|
||||
let isEasyNodeJson = ref(false)
|
||||
|
||||
function handleFromJson(isENJson) {
|
||||
isEasyNodeJson.value = isENJson
|
||||
console.log('isEasyNodeJson:', isEasyNodeJson.value)
|
||||
jsonInputRef.value.click()
|
||||
}
|
||||
|
||||
@ -106,7 +115,8 @@ const handleCsvFile = (event) => {
|
||||
|
||||
Promise.all(readerPromises)
|
||||
.then(jsonContents => {
|
||||
let formatJson = jsonContents.map(item => {
|
||||
let formatJson = jsonContents.flat(Infinity)
|
||||
formatJson = formatJson.map(item => {
|
||||
const { name, host, port, user_name: username } = item
|
||||
return { name, host, port, username }
|
||||
})
|
||||
@ -116,6 +126,9 @@ const handleCsvFile = (event) => {
|
||||
$message.error('导入失败: ', error.message)
|
||||
console.error('导入失败: ', error)
|
||||
})
|
||||
.finally(() => {
|
||||
event.target.value = null
|
||||
})
|
||||
}
|
||||
|
||||
const handleJsonFile = (event) => {
|
||||
@ -143,22 +156,28 @@ const handleJsonFile = (event) => {
|
||||
|
||||
Promise.all(readerPromises)
|
||||
.then(jsonContents => {
|
||||
let formatJson = jsonContents.map(item => {
|
||||
const { name, host, port, user_name: username } = item
|
||||
return { name, host, port, username }
|
||||
})
|
||||
let formatJson = jsonContents.flat(Infinity)
|
||||
if (!isEasyNodeJson.value) {
|
||||
formatJson = formatJson.map(item => {
|
||||
const { name, host, port, user_name: username } = item
|
||||
return { name, host, port, username }
|
||||
})
|
||||
}
|
||||
handleImportHost(formatJson)
|
||||
})
|
||||
.catch(error => {
|
||||
$message.error('导入失败: ', error.message)
|
||||
console.error('导入失败: ', error)
|
||||
})
|
||||
.finally(() => {
|
||||
event.target.value = null
|
||||
})
|
||||
}
|
||||
|
||||
async function handleImportHost(importHost) {
|
||||
// console.log('导入: ', importHost)
|
||||
try {
|
||||
let { data: { len } } = await $api.importHost({ importHost })
|
||||
let { data: { len } } = await $api.importHost({ importHost, isEasyNodeJson: isEasyNodeJson.value })
|
||||
$message({ type: 'success', center: true, message: `成功导入实例: ${ len }台` })
|
||||
emit('update-list')
|
||||
visible.value = false
|
||||
|
@ -2,11 +2,22 @@
|
||||
<div class="server_group_container">
|
||||
<div class="server_group_header">
|
||||
<!-- <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" class="add_host_btn" @click="hostFormVisible = true">添加实例</el-button>
|
||||
<!-- <el-button type="primary" @click="handleHiddenIP">
|
||||
{{ hiddenIp ? '显示IP' : '隐藏IP' }}
|
||||
</el-button> -->
|
||||
<el-button type="primary" @click="importVisible = true">导入实例</el-button>
|
||||
<!-- <el-button type="primary" @click="importVisible = true">导入实例</el-button> -->
|
||||
<el-dropdown trigger="click">
|
||||
<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="importVisible = true">导入实例</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchExport">导出实例</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="primary" class="group_action_btn">
|
||||
批量操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
@ -77,8 +88,9 @@ import HostTable from './components/host-table.vue'
|
||||
import HostForm from './components/host-form.vue'
|
||||
import ImportHost from './components/import-host.vue'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { exportFile } from '@/utils'
|
||||
|
||||
const { proxy: { $api, $store, $router, $message, $messageBox } } = getCurrentInstance()
|
||||
const { proxy: { $api, $store, $router, $message, $messageBox, $tools } } = getCurrentInstance()
|
||||
|
||||
let updateHostData = ref(null)
|
||||
let hostFormVisible = ref(false)
|
||||
@ -155,6 +167,20 @@ let handleBatchOnekey = async () => {
|
||||
$router.push({ path: '/onekey', query: { host: ips, execClientInstallScript: 'true' } })
|
||||
}
|
||||
|
||||
let handleBatchExport = () => {
|
||||
collectSelectHost()
|
||||
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||
console.log(selectHosts.value)
|
||||
let exportData = JSON.parse(JSON.stringify(selectHosts.value))
|
||||
exportData = exportData.map(item => {
|
||||
delete item.monitorData
|
||||
return item
|
||||
})
|
||||
const fileName = `easynode-${ $tools.formatTimestamp(Date.now(), 'time', '.') }.json`
|
||||
exportFile(exportData, fileName, 'application/json')
|
||||
hostTableRefs.value.forEach(item => item.clearSelection())
|
||||
}
|
||||
|
||||
let handleHiddenIP = () => {
|
||||
hiddenIp.value = hiddenIp.value ? 0 : 1
|
||||
localStorage.setItem('hiddenIp', String(hiddenIp.value))
|
||||
@ -207,8 +233,11 @@ let hostFormClosed = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
.add_host_btn {
|
||||
margin-right: 12px;
|
||||
}
|
||||
.group_action_btn {
|
||||
margin: 0 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleUpdateHost(row)"
|
||||
>
|
||||
|
@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- <codemirror /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import codemirror from '@/components/codemirror/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'Test',
|
||||
// components: { codemirror },
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user