新增导出功能&服务器列表排序缓存

This commit is contained in:
chaos-zhu 2024-08-09 11:44:28 +08:00
parent ba67533e9a
commit d83795d7af
9 changed files with 145 additions and 63 deletions

View File

@ -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 } })

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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;
}
}

View File

@ -23,7 +23,7 @@
</el-button>
<el-button
v-else
type="success"
type="primary"
link
@click="handleUpdateHost(row)"
>

View File

@ -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>