✨ 支持实例从csv&json导入
This commit is contained in:
parent
a95caf6d2f
commit
ac7f4eb509
@ -86,38 +86,38 @@ async function removeHost({
|
|||||||
if (hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
|
if (hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
|
||||||
hostList.splice(hostIdx, 1)
|
hostList.splice(hostIdx, 1)
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
// 查询是否存在ssh记录
|
|
||||||
// let sshRecord = await readSSHRecord()
|
|
||||||
// let sshIdx = sshRecord.findIndex(item => item.host === host)
|
|
||||||
// let flag = sshIdx !== -1
|
|
||||||
// if (flag) sshRecord.splice(sshIdx, 1)
|
|
||||||
// writeSSHRecord(sshRecord)
|
|
||||||
|
|
||||||
// res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` })
|
|
||||||
res.success({ data: `${ host }已移除` })
|
res.success({ data: `${ host }已移除` })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原手动排序接口-废弃
|
async function importHost({
|
||||||
// async function updateHostSort({ res, request }) {
|
res, request
|
||||||
// let { body: { list } } = request
|
}) {
|
||||||
// if (!list) return res.fail({ msg: '参数错误' })
|
let { body: { importHost } } = request
|
||||||
// let hostList = await readHostList()
|
if (!Array.isArray(importHost)) return res.fail({ msg: '参数错误' })
|
||||||
// if (hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
|
let hostList = await readHostList()
|
||||||
// let sortResult = []
|
// 过滤已存在的host
|
||||||
// for (let i = 0; i < list.length; i++) {
|
let hostListSet = new Set(hostList.map(item => item.host))
|
||||||
// const curHost = list[i]
|
let newHostList = importHost.filter(item => !hostListSet.has(item.host))
|
||||||
// let temp = hostList.find(({ host }) => curHost.host === host)
|
if (newHostList.length === 0) return res.fail({ msg: '导入的实例已存在' })
|
||||||
// if (!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` })
|
|
||||||
// sortResult.push(temp)
|
let extraFiels = {
|
||||||
// }
|
expired: null, expiredNotify: false, group: 'default', consoleUrl: '', remark: '',
|
||||||
// writeHostList(sortResult)
|
authType: 'privateKey', password: '', privateKey: '', credential: '', command: ''
|
||||||
// res.success({ msg: 'success' })
|
}
|
||||||
// }
|
newHostList = newHostList.map((item, index) => {
|
||||||
|
item.port = Number(item.port) || 0
|
||||||
|
item.index = hostList.length + index + 1
|
||||||
|
return Object.assign(item, { ...extraFiels })
|
||||||
|
})
|
||||||
|
hostList.push(...newHostList)
|
||||||
|
writeHostList(hostList)
|
||||||
|
res.success({ data: { len: newHostList.length } })
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getHostList,
|
getHostList,
|
||||||
addHost,
|
addHost,
|
||||||
updateHost,
|
updateHost,
|
||||||
removeHost
|
removeHost,
|
||||||
// updateHostSort
|
importHost
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh')
|
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh')
|
||||||
const { getHostList, addHost, updateHost, removeHost } = require('../controller/host')
|
const { getHostList, addHost, updateHost, removeHost, importHost } = require('../controller/host')
|
||||||
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
||||||
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
||||||
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
||||||
@ -51,12 +51,12 @@ const host = [
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/host-remove',
|
path: '/host-remove',
|
||||||
controller: removeHost
|
controller: removeHost
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/import-host',
|
||||||
|
controller: importHost
|
||||||
}
|
}
|
||||||
// {
|
|
||||||
// method: 'put',
|
|
||||||
// path: '/host-sort',
|
|
||||||
// controller: updateHostSort
|
|
||||||
// }
|
|
||||||
]
|
]
|
||||||
const user = [
|
const user = [
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
|
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
|
||||||
<title>EasyNode</title>
|
<title>EasyNode</title>
|
||||||
<script src="//at.alicdn.com/t/c/font_3309550_ezojxo1wj3.js"></script>
|
<script src="//at.alicdn.com/t/c/font_3309550_x7zmcgwaxf.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"csv-parse": "^5.5.6",
|
||||||
"element-plus": "^2.7.6",
|
"element-plus": "^2.7.6",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
@ -37,6 +37,9 @@ export default {
|
|||||||
removeHost(data) {
|
removeHost(data) {
|
||||||
return axios({ url: '/host-remove', method: 'post', data })
|
return axios({ url: '/host-remove', method: 'post', data })
|
||||||
},
|
},
|
||||||
|
importHost(data) {
|
||||||
|
return axios({ url: '/import-host', method: 'post', data })
|
||||||
|
},
|
||||||
getPubPem() {
|
getPubPem() {
|
||||||
return axios({ url: '/get-pub-pem', method: 'get' })
|
return axios({ url: '/get-pub-pem', method: 'get' })
|
||||||
},
|
},
|
||||||
|
@ -1,28 +1,78 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" width="600px" top="45px" modal-class="host_form_dialog" append-to-body :title="title"
|
<el-dialog
|
||||||
:close-on-click-modal="false" @open="setDefaultData" @closed="handleClosed">
|
v-model="visible"
|
||||||
<el-form ref="formRef" :model="hostForm" :rules="rules" :hide-required-asterisk="true" label-suffix=":"
|
width="600px"
|
||||||
label-width="100px" :show-message="false">
|
top="45px"
|
||||||
|
modal-class="host_form_dialog"
|
||||||
|
append-to-body
|
||||||
|
:title="title"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@open="setDefaultData"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="hostForm"
|
||||||
|
:rules="rules"
|
||||||
|
:hide-required-asterisk="true"
|
||||||
|
label-suffix=":"
|
||||||
|
label-width="100px"
|
||||||
|
:show-message="false"
|
||||||
|
>
|
||||||
<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="实例分组" style="width: 100%;">
|
||||||
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in groupList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="name" label="名称" prop="name">
|
<el-form-item key="name" label="名称" prop="name">
|
||||||
<el-input v-model.trim="hostForm.name" clearable placeholder="" autocomplete="off" />
|
<el-input
|
||||||
|
v-model.trim="hostForm.name"
|
||||||
|
clearable
|
||||||
|
placeholder=""
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div key="instance_info" class="instance_info">
|
<div key="instance_info" class="instance_info">
|
||||||
<el-form-item key="host" class="form_item_host" label="主机" prop="host">
|
<el-form-item
|
||||||
<el-input v-model.trim="hostForm.host" clearable placeholder="IP" autocomplete="off" />
|
key="host"
|
||||||
|
class="form_item_host"
|
||||||
|
label="主机"
|
||||||
|
prop="host"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model.trim="hostForm.host"
|
||||||
|
clearable
|
||||||
|
placeholder="IP"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="port" class="form_item_port" label="端口" prop="port">
|
<el-form-item
|
||||||
<el-input v-model.trim.number="hostForm.port" clearable placeholder="port" autocomplete="off" />
|
key="port"
|
||||||
|
class="form_item_port"
|
||||||
|
label="端口"
|
||||||
|
prop="port"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model.trim.number="hostForm.port"
|
||||||
|
clearable
|
||||||
|
placeholder="port"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item key="username" label="用户名" prop="username">
|
<el-form-item key="username" label="用户名" prop="username">
|
||||||
<el-autocomplete v-model.trim="hostForm.username" :fetch-suggestions="userSearch" style="width: 100%;"
|
<el-autocomplete
|
||||||
clearable>
|
v-model.trim="hostForm.username"
|
||||||
|
:fetch-suggestions="userSearch"
|
||||||
|
style="width: 100%;"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<div class="value">{{ item.value }}</div>
|
<div class="value">{{ item.value }}</div>
|
||||||
</template>
|
</template>
|
||||||
@ -33,23 +83,56 @@
|
|||||||
<el-radio v-model.trim="hostForm.authType" value="password">密码</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-radio v-model.trim="hostForm.authType" value="credential">凭据</el-radio>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="hostForm.authType === 'privateKey'" key="privateKey" prop="privateKey" label="密钥">
|
<el-form-item
|
||||||
|
v-if="hostForm.authType === 'privateKey'"
|
||||||
|
key="privateKey"
|
||||||
|
prop="privateKey"
|
||||||
|
label="密钥"
|
||||||
|
>
|
||||||
<el-button type="primary" size="small" @click="handleClickUploadBtn">
|
<el-button type="primary" size="small" @click="handleClickUploadBtn">
|
||||||
本地私钥...
|
本地私钥...
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- <el-button type="primary" size="small" @click="handleClickUploadBtn">
|
<!-- <el-button type="primary" size="small" @click="handleClickUploadBtn">
|
||||||
从凭据导入...
|
从凭据导入...
|
||||||
</el-button> -->
|
</el-button> -->
|
||||||
<input ref="privateKeyRef" type="file" name="privateKey" style="display: none;"
|
<input
|
||||||
@change="handleSelectPrivateKeyFile">
|
ref="privateKeyRef"
|
||||||
<el-input v-model.trim="hostForm.privateKey" type="textarea" :rows="5" clearable autocomplete="off"
|
type="file"
|
||||||
style="margin-top: 5px;" placeholder="-----BEGIN RSA PRIVATE KEY-----" />
|
name="privateKey"
|
||||||
|
style="display: none;"
|
||||||
|
@change="handleSelectPrivateKeyFile"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model.trim="hostForm.privateKey"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
clearable
|
||||||
|
autocomplete="off"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
placeholder="-----BEGIN RSA PRIVATE KEY-----"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="hostForm.authType === 'password'" key="password" prop="password" label="密码">
|
<el-form-item
|
||||||
<el-input v-model.trim="hostForm.password" type="password" placeholder="" autocomplete="off" clearable
|
v-if="hostForm.authType === 'password'"
|
||||||
show-password />
|
key="password"
|
||||||
|
prop="password"
|
||||||
|
label="密码"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model.trim="hostForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder=""
|
||||||
|
autocomplete="off"
|
||||||
|
clearable
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="hostForm.authType === 'credential'" key="credential" prop="credential" label="凭据">
|
<el-form-item
|
||||||
|
v-if="hostForm.authType === 'credential'"
|
||||||
|
key="credential"
|
||||||
|
prop="credential"
|
||||||
|
label="凭据"
|
||||||
|
>
|
||||||
<el-select v-model="hostForm.credential" class="credential_select" placeholder="">
|
<el-select v-model="hostForm.credential" class="credential_select" placeholder="">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="empty_credential">
|
<div class="empty_credential">
|
||||||
@ -59,7 +142,12 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-option v-for="item in sshList" :key="item.id" :label="item.name" :value="item.id">
|
<el-option
|
||||||
|
v-for="item in sshList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
>
|
||||||
<div class="auth_type_wrap">
|
<div class="auth_type_wrap">
|
||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
<span class="auth_type_text">
|
<span class="auth_type_text">
|
||||||
@ -70,28 +158,61 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="command" prop="command" label="执行指令">
|
<el-form-item key="command" prop="command" label="执行指令">
|
||||||
<el-input v-model="hostForm.command" type="textarea" :rows="5" clearable autocomplete="off"
|
<el-input
|
||||||
placeholder="连接服务器后自动执行的指令(例如: sudo -i)" />
|
v-model="hostForm.command"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
clearable
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="连接服务器后自动执行的指令(例如: sudo -i)"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item key="expired" label="到期时间" prop="expired">
|
<el-form-item key="expired" label="到期时间" prop="expired">
|
||||||
<el-date-picker v-model="hostForm.expired" type="date" style="width: 100%;" value-format="x"
|
<el-date-picker
|
||||||
placeholder="实例到期时间" />
|
v-model="hostForm.expired"
|
||||||
|
type="date"
|
||||||
|
style="width: 100%;"
|
||||||
|
value-format="x"
|
||||||
|
placeholder="实例到期时间"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="hostForm.expired" key="expiredNotify" label="到期提醒" prop="expiredNotify">
|
<el-form-item
|
||||||
|
v-if="hostForm.expired"
|
||||||
|
key="expiredNotify"
|
||||||
|
label="到期提醒"
|
||||||
|
prop="expiredNotify"
|
||||||
|
>
|
||||||
<el-tooltip content="将在实例到期前7、3、1天发送提醒(需在设置中绑定有效邮箱)" placement="right">
|
<el-tooltip content="将在实例到期前7、3、1天发送提醒(需在设置中绑定有效邮箱)" placement="right">
|
||||||
<el-switch v-model="hostForm.expiredNotify" :active-value="true" :inactive-value="false" />
|
<el-switch v-model="hostForm.expiredNotify" :active-value="true" :inactive-value="false" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="consoleUrl" label="控制台URL" prop="consoleUrl">
|
<el-form-item key="consoleUrl" label="控制台URL" prop="consoleUrl">
|
||||||
<el-input v-model.trim="hostForm.consoleUrl" clearable placeholder="用于直达云服务商控制台" autocomplete="off"
|
<el-input
|
||||||
@keyup.enter="handleSave" />
|
v-model.trim="hostForm.consoleUrl"
|
||||||
|
clearable
|
||||||
|
placeholder="用于直达云服务商控制台"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="handleSave"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="index" label="序号" prop="index">
|
<el-form-item key="index" label="序号" prop="index">
|
||||||
<el-input v-model.trim.number="hostForm.index" clearable placeholder="用于实例列表中排序(填写数字)" autocomplete="off" />
|
<el-input
|
||||||
|
v-model.trim.number="hostForm.index"
|
||||||
|
clearable
|
||||||
|
placeholder="用于实例列表中排序(填写数字)"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item key="remark" label="备注" prop="remark">
|
<el-form-item key="remark" label="备注" prop="remark">
|
||||||
<el-input v-model.trim="hostForm.remark" type="textarea" :rows="3" clearable autocomplete="off"
|
<el-input
|
||||||
placeholder="简单记录实例用途" />
|
v-model.trim="hostForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
clearable
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="简单记录实例用途"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -195,7 +316,7 @@ const handleSelectPrivateKeyFile = (event) => {
|
|||||||
let reader = new FileReader()
|
let reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
hostForm.privateKey = e.target.result
|
hostForm.privateKey = e.target.result
|
||||||
privateKeyRef.value.value = ''
|
privateKeyRef.value = ''
|
||||||
}
|
}
|
||||||
reader.readAsText(file)
|
reader.readAsText(file)
|
||||||
}
|
}
|
||||||
|
221
web/src/views/server/components/import-host.vue
Normal file
221
web/src/views/server/components/import-host.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
width="600px"
|
||||||
|
top="225px"
|
||||||
|
modal-class="import_form_dialog"
|
||||||
|
append-to-body
|
||||||
|
title="导入实例配置"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<h2>选择要导入的文件类型</h2>
|
||||||
|
<ul class="type_list">
|
||||||
|
<li @click="handleFromCsv">
|
||||||
|
<svg-icon name="icon-csv" class="icon" />
|
||||||
|
<span class="from">Xshell</span>
|
||||||
|
<span class="type">(csv)</span>
|
||||||
|
<input
|
||||||
|
ref="csvInputRef"
|
||||||
|
type="file"
|
||||||
|
accept=".csv"
|
||||||
|
multiple
|
||||||
|
name="csvInput"
|
||||||
|
style="display: none;"
|
||||||
|
@change="handleCsvFile"
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li @click="handleFromJson">
|
||||||
|
<svg-icon name="icon-json" class="icon" />
|
||||||
|
<span class="from">FinalShell</span>
|
||||||
|
<span class="type">(json)</span>
|
||||||
|
<input
|
||||||
|
ref="jsonInputRef"
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
multiple
|
||||||
|
name="jsonInput"
|
||||||
|
style="display: none;"
|
||||||
|
@change="handleJsonFile"
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, getCurrentInstance, nextTick } from 'vue'
|
||||||
|
import { RSAEncrypt, AESEncrypt, randomStr } from '@utils/index.js'
|
||||||
|
import { parse } from 'csv-parse/browser/esm/sync'
|
||||||
|
|
||||||
|
const { proxy: { $api, $router, $message, $store } } = getCurrentInstance()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:show', 'update-list',])
|
||||||
|
|
||||||
|
const jsonInputRef = ref(null)
|
||||||
|
const csvInputRef = ref(null)
|
||||||
|
|
||||||
|
let visible = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (newVal) => emit('update:show', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleFromCsv() {
|
||||||
|
csvInputRef.value.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFromJson() {
|
||||||
|
jsonInputRef.value.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCsvFile = (event) => {
|
||||||
|
const files = event.target.files
|
||||||
|
if (!files.length) {
|
||||||
|
console.warn('No files selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const csvFiles = [...files,].filter(file => file.type === 'text/csv')
|
||||||
|
if (csvFiles.length === 0) return $message.warning('未选择有效的CSV文件')
|
||||||
|
|
||||||
|
let readerPromises = csvFiles.map(file => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const csvText = e.target.result
|
||||||
|
try {
|
||||||
|
const jsonContents = parse(csvText, {
|
||||||
|
columns: ['name', 'protocol', 'host', 'port', 'username', 'placeholder',],
|
||||||
|
from_line: 1 // xshell导出的从第一行开始解析
|
||||||
|
})
|
||||||
|
handleImportHost(jsonContents)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error parsing CSV file ${ file.name }:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error(`Failed to read file: ${ file.name }`))
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Promise.all(readerPromises)
|
||||||
|
.then(jsonContents => {
|
||||||
|
let formatJson = jsonContents.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleJsonFile = (event) => {
|
||||||
|
let files = event.target.files
|
||||||
|
let jsonFiles = Array.from(files).filter(file => file.type === 'application/json')
|
||||||
|
if (jsonFiles.length === 0) return $message.warning('未选择有效的JSON文件')
|
||||||
|
|
||||||
|
let readerPromises = jsonFiles.map(file => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
let jsonContent = JSON.parse(e.target.result)
|
||||||
|
resolve(jsonContent)
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error(`Failed to parse JSON file: ${ file.name }`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error(`Failed to read file: ${ file.name }`))
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Promise.all(readerPromises)
|
||||||
|
.then(jsonContents => {
|
||||||
|
let formatJson = jsonContents.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImportHost(importHost) {
|
||||||
|
// console.log('导入: ', importHost)
|
||||||
|
try {
|
||||||
|
let { data: { len } } = await $api.importHost({ importHost })
|
||||||
|
$message({ type: 'success', center: true, message: `成功导入实例: ${ len }台` })
|
||||||
|
emit('update-list')
|
||||||
|
visible.value = false
|
||||||
|
} catch (error) {
|
||||||
|
$message.error('导入失败:', error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.import_form_dialog {
|
||||||
|
h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
margin: 15px 0 25px 0;
|
||||||
|
}
|
||||||
|
.type_list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
li {
|
||||||
|
margin: 0 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
&:hover {
|
||||||
|
background-color: #f1f2f5;
|
||||||
|
color: var(--el-menu-active-color);
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.from {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.type {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog_footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
@ -5,11 +5,14 @@
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="server_group_collapse">
|
<div class="server_group_collapse">
|
||||||
<div v-if="isNoHost">
|
<div v-if="isNoHost">
|
||||||
<el-empty description="暂无实例">
|
<el-empty description="暂无实例">
|
||||||
<el-button type="primary" @click="hostFormVisible = true">添加第一台实例配置</el-button>
|
<el-button type="primary" @click="hostFormVisible = true">添加实例配置</el-button>
|
||||||
|
<span class="or">或</span>
|
||||||
|
<el-button type="primary" @click="importVisible = true">批量导入实例</el-button>
|
||||||
</el-empty>
|
</el-empty>
|
||||||
</div>
|
</div>
|
||||||
<el-collapse v-else v-model="activeGroup">
|
<el-collapse v-else v-model="activeGroup">
|
||||||
@ -38,6 +41,10 @@
|
|||||||
@update-list="handleUpdateList"
|
@update-list="handleUpdateList"
|
||||||
@closed="updateHostData = null"
|
@closed="updateHostData = null"
|
||||||
/>
|
/>
|
||||||
|
<ImportHost
|
||||||
|
v-model:show="importVisible"
|
||||||
|
@update-list="handleUpdateList"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -45,15 +52,18 @@
|
|||||||
import { ref, getCurrentInstance, computed, watch } from 'vue'
|
import { ref, getCurrentInstance, computed, watch } from 'vue'
|
||||||
import HostCard from './components/host-card.vue'
|
import HostCard from './components/host-card.vue'
|
||||||
import HostForm from './components/host-form.vue'
|
import HostForm from './components/host-form.vue'
|
||||||
|
import ImportHost from './components/import-host.vue'
|
||||||
|
|
||||||
const { proxy: { $store, $message } } = getCurrentInstance()
|
const { proxy: { $store, $message } } = getCurrentInstance()
|
||||||
|
|
||||||
const updateHostData = ref(null)
|
let updateHostData = ref(null)
|
||||||
const hostFormVisible = ref(false)
|
let hostFormVisible = ref(false)
|
||||||
const hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
let importVisible = ref(false)
|
||||||
const activeGroup = ref([])
|
|
||||||
|
|
||||||
const handleUpdateList = async () => {
|
let hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
||||||
|
let activeGroup = ref([])
|
||||||
|
|
||||||
|
let handleUpdateList = async () => {
|
||||||
try {
|
try {
|
||||||
await $store.getHostList()
|
await $store.getHostList()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -62,12 +72,12 @@ const handleUpdateList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateHost = (defaultData) => {
|
let handleUpdateHost = (defaultData) => {
|
||||||
hostFormVisible.value = true
|
hostFormVisible.value = true
|
||||||
updateHostData.value = defaultData
|
updateHostData.value = defaultData
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleHiddenIP = () => {
|
let handleHiddenIP = () => {
|
||||||
hiddenIp.value = hiddenIp.value ? 0 : 1
|
hiddenIp.value = hiddenIp.value ? 0 : 1
|
||||||
localStorage.setItem('hiddenIp', String(hiddenIp.value))
|
localStorage.setItem('hiddenIp', String(hiddenIp.value))
|
||||||
}
|
}
|
||||||
@ -124,6 +134,11 @@ let isNoHost = computed(() => Object.keys(groupHostList.value).length === 0)
|
|||||||
.host_card_container {
|
.host_card_container {
|
||||||
padding-top: 25px;
|
padding-top: 25px;
|
||||||
}
|
}
|
||||||
|
.or {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: var(--el-font-size-base);
|
||||||
|
margin: 0 25px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1909,6 +1909,11 @@ csstype@^3.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||||
|
|
||||||
|
csv-parse@^5.5.6:
|
||||||
|
version "5.5.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.6.tgz#0d726d58a60416361358eec291a9f93abe0b6b1a"
|
||||||
|
integrity sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==
|
||||||
|
|
||||||
date-fns@^2.30.0:
|
date-fns@^2.30.0:
|
||||||
version "2.30.0"
|
version "2.30.0"
|
||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user