✨ optimization
This commit is contained in:
parent
f2fe091d2d
commit
62478abf95
@ -12,7 +12,7 @@ async function getIpInfo() {
|
|||||||
consola.success('getIpInfo Success: ', new Date())
|
consola.success('getIpInfo Success: ', new Date())
|
||||||
ipInfo = data
|
ipInfo = data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.error('getIpInfo Error: ', new Date(), error)
|
consola.error('getIpInfo Error: ', new Date(), error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ const getNetIPInfo = async (searchIp = '') => {
|
|||||||
consola.info('查询IP信息:', validInfo)
|
consola.info('查询IP信息:', validInfo)
|
||||||
return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date }
|
return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.error('getIpInfo Error: ', error)
|
// consola.error('getIpInfo Error: ', error)
|
||||||
return {
|
return {
|
||||||
ip: '未知',
|
ip: '未知',
|
||||||
country: '未知',
|
country: '未知',
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
width="80%"
|
width="80%"
|
||||||
:top="'20px'"
|
:top="'30px'"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:close-on-press-escape="false"
|
:close-on-press-escape="false"
|
||||||
:show-close="false"
|
:show-close="false"
|
||||||
@ -10,7 +10,7 @@
|
|||||||
custom-class="container"
|
custom-class="container"
|
||||||
@closed="handleClosed"
|
@closed="handleClosed"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #header>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
FileName - <span>{{ status }}</span>
|
FileName - <span>{{ status }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -30,8 +30,8 @@
|
|||||||
/>
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<footer>
|
<footer>
|
||||||
<div>
|
<div class="select_wrap">
|
||||||
<el-select v-model="curLang" placeholder="Select language" size="small">
|
<el-select v-model="curLang" placeholder="Select language">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in languageKey"
|
v-for="item in languageKey"
|
||||||
:key="item"
|
:key="item"
|
||||||
@ -56,7 +56,7 @@ import languages from './languages'
|
|||||||
import { sortString, getSuffix } from '@/utils'
|
import { sortString, getSuffix } from '@/utils'
|
||||||
|
|
||||||
const languageKey = sortString(Object.keys(languages))
|
const languageKey = sortString(Object.keys(languages))
|
||||||
// console.log('languages: ', languages)
|
// console.log('languages: ', languageKey)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CodeEdit',
|
name: 'CodeEdit',
|
||||||
@ -139,8 +139,8 @@ export default {
|
|||||||
case 'md': return this.curLang = 'markdown'
|
case 'md': return this.curLang = 'markdown'
|
||||||
case 'py': return this.curLang = 'python'
|
case 'py': return this.curLang = 'python'
|
||||||
default:
|
default:
|
||||||
console.log('不支持的文件类型: ', newVal)
|
// console.log('不支持的文件类型: ', newVal)
|
||||||
console.log('默认: ', 'shell')
|
// console.log('默认: ', 'shell')
|
||||||
return this.curLang = 'shell'
|
return this.curLang = 'shell'
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -229,6 +229,10 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
.select_wrap {
|
||||||
|
width: 150px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -1,25 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog v-model="visible" width="800px" :top="'20vh'" :close-on-click-modal="false" :close-on-press-escape="false"
|
||||||
v-model="visible"
|
:show-close="false" center custom-class="container">
|
||||||
width="800px"
|
<template #header>
|
||||||
:top="'20vh'"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
:close-on-press-escape="false"
|
|
||||||
:show-close="false"
|
|
||||||
center
|
|
||||||
custom-class="container"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
输入多行命令发送到终端执行
|
输入多行命令发送到终端执行
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input
|
<el-input v-model="command" :autosize="{ minRows: 10, maxRows: 20 }" type="textarea"
|
||||||
v-model="command"
|
placeholder="Please input command" />
|
||||||
:autosize="{ minRows: 10, maxRows: 20 }"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="Please input command"
|
|
||||||
/>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<footer>
|
<footer>
|
||||||
<div class="btns">
|
<div class="btns">
|
||||||
@ -30,48 +18,42 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
<script>
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
export default {
|
required: true,
|
||||||
name: 'InputCommand',
|
type: Boolean
|
||||||
props: {
|
|
||||||
show: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['update:show', 'closed', 'input-command',],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
command: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
visible: {
|
|
||||||
get() {
|
|
||||||
return this.show
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
this.$emit('update:show', newVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleSave() {
|
|
||||||
this.$emit('input-command', this.command)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show', 'closed', 'input-command'])
|
||||||
|
|
||||||
|
const command = ref('')
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get() {
|
||||||
|
return props.show
|
||||||
|
},
|
||||||
|
set(newVal) {
|
||||||
|
emit('update:show', newVal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
emit('input-command', command.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
</style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.container {
|
.container {
|
||||||
.el-dialog__header {
|
.el-dialog__header {
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -80,17 +62,21 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
padding: 10px!important;
|
padding: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__footer {
|
.el-dialog__footer {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.btns {
|
.btns {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
import hostList from '@views/list/index.vue'
|
// import hostList from '@views/list/index.vue'
|
||||||
import login from '@views/login/index.vue'
|
// import login from '@views/login/index.vue'
|
||||||
import terminal from '@views/terminal/index.vue'
|
// import terminal from '@views/terminal/index.vue'
|
||||||
import test from '@views/test/index.vue'
|
// import test from '@views/test/index.vue'
|
||||||
|
|
||||||
// const hostList = () => import('@views/list/index.vue')
|
const hostList = () => import('@views/list/index.vue')
|
||||||
// const login = () => import('@views/login/index.vue')
|
const login = () => import('@views/login/index.vue')
|
||||||
// const terminal = () => import('@views/terminal/index.vue')
|
const terminal = () => import('@views/terminal/index.vue')
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: hostList },
|
{ path: '/', component: hostList },
|
||||||
{ path: '/login', component: login },
|
{ path: '/login', component: login },
|
||||||
{ path: '/terminal', component: terminal },
|
{ path: '/terminal', component: terminal },
|
||||||
{ path: '/test', component: test },
|
// { path: '/test', component: test },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default createRouter({
|
export default createRouter({
|
||||||
|
@ -6,16 +6,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="weizhi field">
|
<div class="weizhi field">
|
||||||
<el-popover
|
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
||||||
placement="bottom-start"
|
|
||||||
:width="200"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<svg-icon
|
<svg-icon name="icon-fuwuqi" class="svg-icon" />
|
||||||
name="icon-fuwuqi"
|
|
||||||
class="svg-icon"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="field-detail">
|
<div class="field-detail">
|
||||||
<h2>系统</h2>
|
<h2>系统</h2>
|
||||||
@ -40,16 +33,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="weizhi field">
|
<div class="weizhi field">
|
||||||
<el-popover
|
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
||||||
placement="bottom-start"
|
|
||||||
:width="200"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<svg-icon
|
<svg-icon name="icon-position" class="svg-icon" />
|
||||||
name="icon-position"
|
|
||||||
class="svg-icon"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="field-detail">
|
<div class="field-detail">
|
||||||
<h2>位置信息</h2>
|
<h2>位置信息</h2>
|
||||||
@ -61,22 +47,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<span>{{ `${ipInfo?.country || '--' } ${ipInfo?.regionName || '--'}` }}</span>
|
<span>{{ `${ipInfo?.country || '--'} ${ipInfo?.regionName || '--'}` }}</span>
|
||||||
<!-- <span>{{ `${ipInfo?.country || '--' } ${ipInfo?.regionName || '--'} ${ipInfo?.city || '--'}` }}</span> -->
|
<!-- <span>{{ `${ipInfo?.country || '--' } ${ipInfo?.regionName || '--'} ${ipInfo?.city || '--'}` }}</span> -->
|
||||||
<span>{{ hostIp }}</span>
|
<span>{{ hostIp }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cpu field">
|
<div class="cpu field">
|
||||||
<el-popover
|
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
||||||
placement="bottom-start"
|
|
||||||
:width="200"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<svg-icon
|
<svg-icon name="icon-xingzhuang" class="svg-icon" />
|
||||||
name="icon-xingzhuang"
|
|
||||||
class="svg-icon"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="field-detail">
|
<div class="field-detail">
|
||||||
<h2>CPU</h2>
|
<h2>CPU</h2>
|
||||||
@ -86,21 +65,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<span :style="{color: setColor(cpuInfo.cpuUsage)}">{{ cpuInfo.cpuUsage || '0' || '--' }}%</span>
|
<span :style="{ color: setColor(cpuInfo.cpuUsage) }">{{ cpuInfo.cpuUsage || '0' || '--' }}%</span>
|
||||||
<span>{{ cpuInfo.cpuCount || '--' }} 核心</span>
|
<span>{{ cpuInfo.cpuCount || '--' }} 核心</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ram field">
|
<div class="ram field">
|
||||||
<el-popover
|
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
||||||
placement="bottom-start"
|
|
||||||
:width="200"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<svg-icon
|
<svg-icon name="icon-neicun1" class="svg-icon" />
|
||||||
name="icon-neicun1"
|
|
||||||
class="svg-icon"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="field-detail">
|
<div class="field-detail">
|
||||||
<h2>内存</h2>
|
<h2>内存</h2>
|
||||||
@ -111,21 +83,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<span :style="{color: setColor(memInfo.usedMemPercentage)}">{{ $tools.toFixed(memInfo.usedMemPercentage) }}%</span>
|
<span :style="{ color: setColor(memInfo.usedMemPercentage) }">{{ $tools.toFixed(memInfo.usedMemPercentage)
|
||||||
|
}}%</span>
|
||||||
<span>{{ $tools.toFixed(memInfo.usedMemMb / 1024) }} | {{ $tools.toFixed(memInfo.totalMemMb / 1024) }} GB</span>
|
<span>{{ $tools.toFixed(memInfo.usedMemMb / 1024) }} | {{ $tools.toFixed(memInfo.totalMemMb / 1024) }} GB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="yingpan field">
|
<div class="yingpan field">
|
||||||
<el-popover
|
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
||||||
placement="bottom-start"
|
|
||||||
:width="200"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<svg-icon
|
<svg-icon name="icon-xingzhuang1" class="svg-icon" />
|
||||||
name="icon-xingzhuang1"
|
|
||||||
class="svg-icon"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="field-detail">
|
<div class="field-detail">
|
||||||
<h2>存储</h2>
|
<h2>存储</h2>
|
||||||
@ -136,21 +102,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<span :style="{color: setColor(driveInfo.usedPercentage)}">{{ driveInfo.usedPercentage || '--' }}%</span>
|
<span :style="{ color: setColor(driveInfo.usedPercentage) }">{{ driveInfo.usedPercentage || '--' }}%</span>
|
||||||
<span>{{ driveInfo.usedGb || '--' }} | {{ driveInfo.totalGb || '--' }} GB</span>
|
<span>{{ driveInfo.usedGb || '--' }} | {{ driveInfo.totalGb || '--' }} GB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wangluo field">
|
<div class="wangluo field">
|
||||||
<el-popover
|
<el-popover placement="bottom-start" :width="200" trigger="hover">
|
||||||
placement="bottom-start"
|
|
||||||
:width="200"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<svg-icon
|
<svg-icon name="icon-wangluo1" class="svg-icon" />
|
||||||
name="icon-wangluo1"
|
|
||||||
class="svg-icon"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="field-detail">
|
<div class="field-detail">
|
||||||
<h2>网卡</h2>
|
<h2>网卡</h2>
|
||||||
@ -159,11 +118,7 @@
|
|||||||
<div>↑ {{ $tools.toFixed(netstatInfo.netTotal?.outputMb) || 0 }}MB / s</div>
|
<div>↑ {{ $tools.toFixed(netstatInfo.netTotal?.outputMb) || 0 }}MB / s</div>
|
||||||
<div>↓ {{ $tools.toFixed(netstatInfo.netTotal?.inputMb) || 0 }}MB / s</div>
|
<div>↓ {{ $tools.toFixed(netstatInfo.netTotal?.inputMb) || 0 }}MB / s</div>
|
||||||
</h3> -->
|
</h3> -->
|
||||||
<div
|
<div v-for="(value, key) in netstatInfo.netCards" :key="key" style="display: flex; flex-direction: column;">
|
||||||
v-for="(value, key) in netstatInfo.netCards"
|
|
||||||
:key="key"
|
|
||||||
style="display: flex; flex-direction: column;"
|
|
||||||
>
|
|
||||||
<h3>
|
<h3>
|
||||||
<span>{{ key }}</span>
|
<span>{{ key }}</span>
|
||||||
<div>↑ {{ $tools.formatNetSpeed(value?.outputMb) || 0 }}</div>
|
<div>↑ {{ $tools.formatNetSpeed(value?.outputMb) || 0 }}</div>
|
||||||
@ -178,11 +133,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fields terminal">
|
<div class="fields terminal">
|
||||||
<el-dropdown
|
<el-dropdown class="web-ssh" type="primary" trigger="click">
|
||||||
class="web-ssh"
|
|
||||||
type="primary"
|
|
||||||
trigger="click"
|
|
||||||
>
|
|
||||||
<!-- <el-button type="primary" @click="handleSSH">Web SSH</el-button> -->
|
<!-- <el-button type="primary" @click="handleSSH">Web SSH</el-button> -->
|
||||||
<el-button type="primary">功能</el-button>
|
<el-button type="primary">功能</el-button>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
@ -197,183 +148,135 @@
|
|||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SSHForm
|
<SSHForm v-model:show="sshFormVisible" :temp-host="tempHost" :name="name" />
|
||||||
v-model:show="sshFormVisible"
|
|
||||||
:temp-host="tempHost"
|
|
||||||
:name="name"
|
|
||||||
/>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, getCurrentInstance } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import SSHForm from './ssh-form.vue'
|
import SSHForm from './ssh-form.vue'
|
||||||
|
|
||||||
export default {
|
const { proxy: { $api, $tools } } = getCurrentInstance()
|
||||||
name: 'HostCard',
|
|
||||||
components: {
|
const props = defineProps({
|
||||||
SSHForm
|
hostInfo: {
|
||||||
|
required: true,
|
||||||
|
type: Object
|
||||||
},
|
},
|
||||||
props: {
|
hiddenIp: {
|
||||||
hostInfo: {
|
required: true,
|
||||||
required: true,
|
type: [Number, Boolean]
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
hiddenIp: {
|
|
||||||
required: true,
|
|
||||||
type: [Number, Boolean,]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['update-list', 'update-host',],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
sshFormVisible: false,
|
|
||||||
tempHost: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
hostIp() {
|
|
||||||
let ip = this.ipInfo?.query || this.host || '--'
|
|
||||||
try {
|
|
||||||
// let formatIp = ip.replace(/(?<=\d*\.\d*\.)(\d*)/g, (matchStr) => matchStr.replace(/\d/g, '*'))
|
|
||||||
let formatIp = ip.replace(/\d/g, '*').split('.').map((item) => item.padStart(3, '*')).join('.')
|
|
||||||
return this.hiddenIp ? formatIp : ip
|
|
||||||
} catch (error) {
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
},
|
|
||||||
host() {
|
|
||||||
return this.hostInfo?.host
|
|
||||||
},
|
|
||||||
name() {
|
|
||||||
return this.hostInfo?.name
|
|
||||||
},
|
|
||||||
ping() {
|
|
||||||
return this.hostInfo?.ping || ''
|
|
||||||
},
|
|
||||||
expiredTime() {
|
|
||||||
return this.$tools.formatTimestamp(this.hostInfo?.expired, 'date')
|
|
||||||
},
|
|
||||||
consoleUrl() {
|
|
||||||
return this.hostInfo?.consoleUrl
|
|
||||||
},
|
|
||||||
ipInfo() {
|
|
||||||
return this.hostInfo?.ipInfo || {}
|
|
||||||
},
|
|
||||||
isError() {
|
|
||||||
return !Boolean(this.hostInfo?.osInfo) // 没获取系统信息默认未连接
|
|
||||||
},
|
|
||||||
cpuInfo() {
|
|
||||||
return this.hostInfo?.cpuInfo || {}
|
|
||||||
},
|
|
||||||
memInfo() {
|
|
||||||
return this.hostInfo?.memInfo || {}
|
|
||||||
},
|
|
||||||
osInfo() {
|
|
||||||
return this.hostInfo?.osInfo || {}
|
|
||||||
},
|
|
||||||
driveInfo() {
|
|
||||||
return this.hostInfo?.driveInfo || {}
|
|
||||||
},
|
|
||||||
netstatInfo() {
|
|
||||||
let { total: netTotal, ...netCards } = this.hostInfo?.netstatInfo || {}
|
|
||||||
return { netTotal, netCards: netCards || {} }
|
|
||||||
},
|
|
||||||
openedCount() {
|
|
||||||
return this.hostInfo?.openedCount || 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// if (data?.message === 'private range') {
|
|
||||||
// data.country = '本地'
|
|
||||||
// data.city = '局域网'
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setColor(num){
|
|
||||||
num = Number(num)
|
|
||||||
return num ? (num < 80 ? '#595959' : ((num >= 80 && num < 90) ? '#FF6600' : '#FF0000')) : '#595959'
|
|
||||||
},
|
|
||||||
handleUpdate() {
|
|
||||||
let { name, host, hostInfo: { expired, expiredNotify, group, consoleUrl, remark } } = this
|
|
||||||
this.$emit('update-host', { name, host, expired, expiredNotify, group, consoleUrl, remark })
|
|
||||||
},
|
|
||||||
handleToConsole() {
|
|
||||||
window.open(this.consoleUrl)
|
|
||||||
},
|
|
||||||
async handleSSH() {
|
|
||||||
let { host, name } = this
|
|
||||||
let { data } = await this.$api.existSSH(host)
|
|
||||||
console.log('是否存在凭证:', data)
|
|
||||||
if (data) return window.open(`/terminal?host=${ host }&name=${ name }`)
|
|
||||||
if (!host) {
|
|
||||||
return ElMessage({
|
|
||||||
message: '请等待获取服务器ip或刷新页面重试',
|
|
||||||
type: 'warning',
|
|
||||||
center: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.tempHost = host
|
|
||||||
this.sshFormVisible = true
|
|
||||||
},
|
|
||||||
async handleRemoveSSH() {
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
'确认删除SSH凭证',
|
|
||||||
'Warning',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(async () => {
|
|
||||||
let { host } = this
|
|
||||||
let { data } = await this.$api.removeSSH(host)
|
|
||||||
ElMessage({
|
|
||||||
message: data,
|
|
||||||
type: 'success',
|
|
||||||
center: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleRemoveHost() {
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
'确认删除主机',
|
|
||||||
'Warning',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(async () => {
|
|
||||||
let { host } = this
|
|
||||||
let { data } = await this.$api.removeHost({ host })
|
|
||||||
ElMessage({
|
|
||||||
message: data,
|
|
||||||
type: 'success',
|
|
||||||
center: true
|
|
||||||
})
|
|
||||||
this.$emit('update-list')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update-list', 'update-host'])
|
||||||
|
|
||||||
|
const sshFormVisible = ref(false)
|
||||||
|
const tempHost = ref('')
|
||||||
|
|
||||||
|
const hostIp = computed(() => {
|
||||||
|
let ip = props.hostInfo?.ipInfo?.query || props.hostInfo?.host || '--'
|
||||||
|
try {
|
||||||
|
let formatIp = ip.replace(/\d/g, '*').split('.').map((item) => item.padStart(3, '*')).join('.')
|
||||||
|
return props.hiddenIp ? formatIp : ip
|
||||||
|
} catch (error) {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const host = computed(() => props.hostInfo?.host)
|
||||||
|
const name = computed(() => props.hostInfo?.name)
|
||||||
|
const ping = computed(() => props.hostInfo?.ping || '')
|
||||||
|
const expiredTime = computed(() => $tools.formatTimestamp(props.hostInfo?.expired, 'date'))
|
||||||
|
const consoleUrl = computed(() => props.hostInfo?.consoleUrl)
|
||||||
|
const ipInfo = computed(() => props.hostInfo?.ipInfo || {})
|
||||||
|
const isError = computed(() => !Boolean(props.hostInfo?.osInfo))
|
||||||
|
const cpuInfo = computed(() => props.hostInfo?.cpuInfo || {})
|
||||||
|
const memInfo = computed(() => props.hostInfo?.memInfo || {})
|
||||||
|
const osInfo = computed(() => props.hostInfo?.osInfo || {})
|
||||||
|
const driveInfo = computed(() => props.hostInfo?.driveInfo || {})
|
||||||
|
const netstatInfo = computed(() => {
|
||||||
|
let { total: netTotal, ...netCards } = props.hostInfo?.netstatInfo || {}
|
||||||
|
return { netTotal, netCards: netCards || {} }
|
||||||
|
})
|
||||||
|
const openedCount = computed(() => props.hostInfo?.openedCount || 0)
|
||||||
|
|
||||||
|
const setColor = (num) => {
|
||||||
|
num = Number(num)
|
||||||
|
return num ? (num < 80 ? '#595959' : (num >= 80 && num < 90 ? '#FF6600' : '#FF0000')) : '#595959'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdate = () => {
|
||||||
|
let { expired, expiredNotify, group, consoleUrl, remark } = props.hostInfo
|
||||||
|
emit('update-host', { name: name.value, host: host.value, expired, expiredNotify, group, consoleUrl, remark })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToConsole = () => {
|
||||||
|
window.open(consoleUrl.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSSH = async () => {
|
||||||
|
let { data } = await $api.existSSH(host.value)
|
||||||
|
if (data) return window.open(`/terminal?host=${host.value}&name=${name.value}`)
|
||||||
|
if (!host.value) {
|
||||||
|
return ElMessage({
|
||||||
|
message: '请等待获取服务器ip或刷新页面重试',
|
||||||
|
type: 'warning',
|
||||||
|
center: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
tempHost.value = host.value
|
||||||
|
sshFormVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveSSH = async () => {
|
||||||
|
ElMessageBox.confirm('确认删除SSH凭证', 'Warning', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
let { data } = await $api.removeSSH(host.value)
|
||||||
|
ElMessage({
|
||||||
|
message: data,
|
||||||
|
type: 'success',
|
||||||
|
center: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveHost = async () => {
|
||||||
|
ElMessageBox.confirm('确认删除主机', 'Warning', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
let { data } = await $api.removeHost({ host: host.value })
|
||||||
|
ElMessage({
|
||||||
|
message: data,
|
||||||
|
type: 'success',
|
||||||
|
center: true
|
||||||
|
})
|
||||||
|
emit('update-list')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.host-card {
|
.host-card {
|
||||||
margin: 0px 30px 20px;
|
margin: 0px 30px 20px;
|
||||||
transition: all 0.5s;
|
transition: all 0.5s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow:0px 0px 15px rgba(6, 30, 37, 0.5);
|
box-shadow: 0px 0px 15px rgba(6, 30, 37, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.host-state {
|
.host-state {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
// transform: rotate(-45deg);
|
// transform: rotate(-45deg);
|
||||||
@ -381,35 +284,43 @@ export default {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.online {
|
.online {
|
||||||
color: #009933;
|
color: #009933;
|
||||||
background-color: #e8fff3;
|
background-color: #e8fff3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offline {
|
.offline {
|
||||||
color: #FF0033;
|
color: #FF0033;
|
||||||
background-color: #fff5f8;
|
background-color: #fff5f8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
& > div {
|
|
||||||
|
&>div {
|
||||||
flex: 1
|
flex: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
color: #1989fa;
|
color: #1989fa;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
// justify-content: center;
|
// justify-content: center;
|
||||||
span {
|
span {
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
@ -418,17 +329,21 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #595959;
|
color: #595959;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 19px;
|
height: 19px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
text-decoration-color: #1989fa;
|
text-decoration-color: #1989fa;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
display: none;
|
display: none;
|
||||||
width: 13px;
|
width: 13px;
|
||||||
@ -437,7 +352,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.web-ssh {
|
.web-ssh {
|
||||||
|
|
||||||
// ::v-deep has been deprecated. Use :deep(<inner-selector>) instead.
|
// ::v-deep has been deprecated. Use :deep(<inner-selector>) instead.
|
||||||
:deep(.el-dropdown__caret-button) {
|
:deep(.el-dropdown__caret-button) {
|
||||||
margin-left: -5px;
|
margin-left: -5px;
|
||||||
@ -451,17 +368,20 @@ export default {
|
|||||||
.field-detail {
|
.field-detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0px 0 8px 0;
|
margin: 0px 0 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
span {
|
span {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #797979;
|
color: #797979;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
@closed="handleClosed"
|
@closed="handleClosed"
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="form"
|
ref="formRef"
|
||||||
:model="hostForm"
|
:model="hostForm"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
:hide-required-asterisk="true"
|
:hide-required-asterisk="true"
|
||||||
@ -103,111 +103,100 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
const resetForm = () => {
|
import { ref, reactive, computed, watch, getCurrentInstance, nextTick } from 'vue'
|
||||||
return {
|
|
||||||
group: 'default',
|
|
||||||
name: '',
|
|
||||||
host: '',
|
|
||||||
expired: null,
|
|
||||||
expiredNotify: false,
|
|
||||||
consoleUrl: '',
|
|
||||||
remark: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default {
|
|
||||||
name: 'HostForm',
|
|
||||||
props: {
|
|
||||||
show: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
defaultData: {
|
|
||||||
required: false,
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['update:show', 'update-list', 'closed',],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hostForm: resetForm(),
|
|
||||||
oldHost: '',
|
|
||||||
groupList: [],
|
|
||||||
rules: {
|
|
||||||
group: { required: true, message: '选择一个分组' },
|
|
||||||
name: { required: true, message: '输入主机别名', trigger: 'change' },
|
|
||||||
host: { required: true, message: '输入IP/域名', trigger: 'change' },
|
|
||||||
expired: { required: false },
|
|
||||||
expiredNotify: { required: false },
|
|
||||||
consoleUrl: { required: false },
|
|
||||||
remark: { required: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
visible: {
|
|
||||||
get() {
|
|
||||||
return this.show
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
this.$emit('update:show', newVal)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title() {
|
|
||||||
return this.defaultData ? '修改服务器' : '新增服务器'
|
|
||||||
},
|
|
||||||
formRef() {
|
|
||||||
return this.$refs['form']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
show(newVal) {
|
|
||||||
if(!newVal) return
|
|
||||||
this.getGroupList()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getGroupList() {
|
|
||||||
this.$api.getGroupList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
this.groupList = data
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleClosed() {
|
|
||||||
console.log('handleClosed')
|
|
||||||
this.hostForm = resetForm()
|
|
||||||
this.$emit('closed')
|
|
||||||
this.$nextTick(() => this.formRef.resetFields())
|
|
||||||
},
|
|
||||||
setDefaultData() {
|
|
||||||
if(!this.defaultData) return
|
|
||||||
let { name, host, expired, expiredNotify, consoleUrl, group, remark } = this.defaultData
|
|
||||||
this.oldHost = host // 保存旧的host用于后端查找
|
|
||||||
this.hostForm = { name, host, expired, expiredNotify, consoleUrl, group, remark }
|
|
||||||
|
|
||||||
},
|
const { proxy: { $api, $message } } = getCurrentInstance()
|
||||||
handleSave() {
|
|
||||||
this.formRef.validate()
|
const props = defineProps({
|
||||||
.then(async () => {
|
show: {
|
||||||
if(!this.hostForm.expired || !this.hostForm.expiredNotify) {
|
required: true,
|
||||||
this.hostForm.expired = null
|
type: Boolean
|
||||||
this.hostForm.expiredNotify = false
|
},
|
||||||
}
|
defaultData: {
|
||||||
if(this.defaultData) {
|
required: false,
|
||||||
let { oldHost } = this
|
type: Object,
|
||||||
let { msg } = await this.$api.updateHost(Object.assign({}, this.hostForm, { oldHost }))
|
default: null
|
||||||
this.$message({ type: 'success', center: true, message: msg })
|
|
||||||
}else {
|
|
||||||
let { msg } = await this.$api.saveHost(this.hostForm)
|
|
||||||
this.$message({ type: 'success', center: true, message: msg })
|
|
||||||
}
|
|
||||||
this.visible = false
|
|
||||||
this.$emit('update-list')
|
|
||||||
this.hostForm = resetForm()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:show', 'update-list', 'closed'])
|
||||||
|
|
||||||
|
const resetForm = () => ({
|
||||||
|
group: 'default',
|
||||||
|
name: '',
|
||||||
|
host: '',
|
||||||
|
expired: null,
|
||||||
|
expiredNotify: false,
|
||||||
|
consoleUrl: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const hostForm = reactive(resetForm())
|
||||||
|
const oldHost = ref('')
|
||||||
|
const groupList = ref([])
|
||||||
|
const rules = reactive({
|
||||||
|
group: { required: true, message: '选择一个分组' },
|
||||||
|
name: { required: true, message: '输入主机别名', trigger: 'change' },
|
||||||
|
host: { required: true, message: '输入IP/域名', trigger: 'change' },
|
||||||
|
expired: { required: false },
|
||||||
|
expiredNotify: { required: false },
|
||||||
|
consoleUrl: { required: false },
|
||||||
|
remark: { required: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (newVal) => emit('update:show', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = computed(() => props.defaultData ? '修改服务器' : '新增服务器')
|
||||||
|
|
||||||
|
watch(() => props.show, (newVal) => {
|
||||||
|
if (!newVal) return
|
||||||
|
getGroupList()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getGroupList = () => {
|
||||||
|
$api.getGroupList()
|
||||||
|
.then(({ data }) => {
|
||||||
|
groupList.value = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClosed = () => {
|
||||||
|
// console.log('handleClosed')
|
||||||
|
Object.assign(hostForm, resetForm())
|
||||||
|
emit('closed')
|
||||||
|
nextTick(() => formRef.value.resetFields())
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDefaultData = () => {
|
||||||
|
if (!props.defaultData) return
|
||||||
|
let { name, host, expired, expiredNotify, consoleUrl, group, remark } = props.defaultData
|
||||||
|
oldHost.value = host
|
||||||
|
Object.assign(hostForm, { name, host, expired, expiredNotify, consoleUrl, group, remark })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
formRef.value.validate()
|
||||||
|
.then(async () => {
|
||||||
|
if (!hostForm.expired || !hostForm.expiredNotify) {
|
||||||
|
hostForm.expired = null
|
||||||
|
hostForm.expiredNotify = false
|
||||||
|
}
|
||||||
|
if (props.defaultData) {
|
||||||
|
let { msg } = await $api.updateHost(Object.assign({}, hostForm, { oldHost: oldHost.value }))
|
||||||
|
$message({ type: 'success', center: true, message: msg })
|
||||||
|
} else {
|
||||||
|
let { msg } = await $api.saveHost(hostForm)
|
||||||
|
$message({ type: 'success', center: true, message: msg })
|
||||||
|
}
|
||||||
|
visible.value = false
|
||||||
|
emit('update-list')
|
||||||
|
Object.assign(hostForm, resetForm())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-loading="loading">
|
<div v-loading="loading">
|
||||||
<el-form
|
<el-form
|
||||||
ref="email-form"
|
ref="emailFormRef"
|
||||||
:model="emailForm"
|
:model="emailForm"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
:inline="true"
|
:inline="true"
|
||||||
@ -61,7 +61,7 @@
|
|||||||
<el-table-column prop="email" label="Email" />
|
<el-table-column prop="email" label="Email" />
|
||||||
<el-table-column prop="name" label="服务商" />
|
<el-table-column prop="name" label="服务商" />
|
||||||
<el-table-column label="操作">
|
<el-table-column label="操作">
|
||||||
<template #default="{row}">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="row.loading"
|
:loading="row.loading"
|
||||||
@ -81,99 +81,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
|
||||||
name: 'UserEmailList',
|
|
||||||
data() {
|
const { proxy: { $api, $message, $messageBox, $notification } } = getCurrentInstance()
|
||||||
return {
|
|
||||||
loading: false,
|
const loading = ref(false)
|
||||||
userEmailList: [],
|
const userEmailList = ref([])
|
||||||
supportEmailList: [],
|
const supportEmailList = ref([])
|
||||||
emailForm: {
|
const emailFormRef = ref(null)
|
||||||
target: 'qq',
|
|
||||||
auth: {
|
const emailForm = reactive({
|
||||||
user: '',
|
target: 'qq',
|
||||||
pass: ''
|
auth: {
|
||||||
}
|
user: '',
|
||||||
},
|
pass: ''
|
||||||
rules: {
|
|
||||||
'auth.user': { required: true, type: 'email', message: '需输入邮箱', trigger: 'change' },
|
|
||||||
'auth.pass': { required: true, message: '需输入SMTP授权码', trigger: 'change' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getUserEmailList()
|
|
||||||
this.getSupportEmailList()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getUserEmailList() {
|
|
||||||
this.loading = true
|
|
||||||
this.$api.getUserEmailList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
this.userEmailList = data.map(item => {
|
|
||||||
item.loading = false
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => this.loading = false)
|
|
||||||
},
|
|
||||||
getSupportEmailList() {
|
|
||||||
this.$api.getSupportEmailList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
this.supportEmailList = data
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addEmail() {
|
|
||||||
let emailFormRef = this.$refs['email-form']
|
|
||||||
emailFormRef.validate()
|
|
||||||
.then(() => {
|
|
||||||
this.$api.updateUserEmailList({ ...this.emailForm })
|
|
||||||
.then(() => {
|
|
||||||
this.$message.success('添加成功, 点击[测试]按钮发送测试邮件')
|
|
||||||
let { target } = this.emailForm
|
|
||||||
this.emailForm = { target, auth: { user: '', pass: '' } }
|
|
||||||
this.$nextTick(() => emailFormRef.resetFields())
|
|
||||||
this.getUserEmailList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
pushTestEmail(row) {
|
|
||||||
row.loading = true
|
|
||||||
const { email: toEmail } = row
|
|
||||||
this.$api.pushTestEmail({ isTest: true, toEmail })
|
|
||||||
.then(() => {
|
|
||||||
this.$message.success(`发送成功, 请检查邮箱: ${ toEmail }`)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.$notification({
|
|
||||||
title: '发送测试邮件失败, 请检查邮箱SMTP配置',
|
|
||||||
message: error.response?.data.msg,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
row.loading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteUserEmail({ email }) {
|
|
||||||
this.$messageBox.confirm(
|
|
||||||
`确认删除邮箱:${ email }`,
|
|
||||||
'Warning',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(async () => {
|
|
||||||
await this.$api.deleteUserEmail(email)
|
|
||||||
this.$message.success('success')
|
|
||||||
this.getUserEmailList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
'auth.user': { required: true, type: 'email', message: '需输入邮箱', trigger: 'change' },
|
||||||
|
'auth.pass': { required: true, message: '需输入SMTP授权码', trigger: 'change' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const getUserEmailList = () => {
|
||||||
|
loading.value = true
|
||||||
|
$api.getUserEmailList()
|
||||||
|
.then(({ data }) => {
|
||||||
|
userEmailList.value = data.map(item => {
|
||||||
|
item.loading = false
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => loading.value = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSupportEmailList = () => {
|
||||||
|
$api.getSupportEmailList()
|
||||||
|
.then(({ data }) => {
|
||||||
|
supportEmailList.value = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addEmail = () => {
|
||||||
|
emailFormRef.value.validate()
|
||||||
|
.then(() => {
|
||||||
|
$api.updateUserEmailList({ ...emailForm })
|
||||||
|
.then(() => {
|
||||||
|
$message.success('添加成功, 点击[测试]按钮发送测试邮件')
|
||||||
|
let { target } = emailForm
|
||||||
|
emailForm.target = target
|
||||||
|
emailForm.auth.user = ''
|
||||||
|
emailForm.auth.pass = ''
|
||||||
|
getUserEmailList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushTestEmail = (row) => {
|
||||||
|
row.loading = true
|
||||||
|
const { email: toEmail } = row
|
||||||
|
$api.pushTestEmail({ isTest: true, toEmail })
|
||||||
|
.then(() => {
|
||||||
|
$message.success(`发送成功, 请检查邮箱: ${toEmail}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
$notification({
|
||||||
|
title: '发送测试邮件失败, 请检查邮箱SMTP配置',
|
||||||
|
message: error.response?.data.msg,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
row.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteUserEmail = ({ email }) => {
|
||||||
|
$messageBox.confirm(
|
||||||
|
`确认删除邮箱:${email}`,
|
||||||
|
'Warning',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
await $api.deleteUserEmail(email)
|
||||||
|
$message.success('success')
|
||||||
|
getUserEmailList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getUserEmailList()
|
||||||
|
getSupportEmailList()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form
|
<el-form
|
||||||
ref="group-form"
|
ref="groupFormRef"
|
||||||
:model="groupForm"
|
:model="groupForm"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
:inline="true"
|
:inline="true"
|
||||||
@ -17,7 +17,6 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="" prop="index" style="width: 200px;">
|
<el-form-item label="" prop="index" style="width: 200px;">
|
||||||
<!-- <el-input-number v-model="groupForm.index" :min="1" :max="10" /> -->
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model.number="groupForm.index"
|
v-model.number="groupForm.index"
|
||||||
clearable
|
clearable
|
||||||
@ -51,7 +50,7 @@
|
|||||||
<el-table-column prop="id" label="ID" />
|
<el-table-column prop="id" label="ID" />
|
||||||
<el-table-column prop="name" label="分组名称" />
|
<el-table-column prop="name" label="分组名称" />
|
||||||
<el-table-column label="关联服务器数量">
|
<el-table-column label="关联服务器数量">
|
||||||
<template #default="{row}">
|
<template #default="{ row }">
|
||||||
<el-popover
|
<el-popover
|
||||||
v-if="row.hosts.list.length !== 0"
|
v-if="row.hosts.list.length !== 0"
|
||||||
placement="right"
|
placement="right"
|
||||||
@ -73,7 +72,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作">
|
<el-table-column label="操作">
|
||||||
<template #default="{row}">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" @click="handleChange(row)">修改</el-button>
|
<el-button type="primary" @click="handleChange(row)">修改</el-button>
|
||||||
<el-button v-show="row.id !== 'default'" type="danger" @click="deleteGroup(row)">删除</el-button>
|
<el-button v-show="row.id !== 'default'" type="danger" @click="deleteGroup(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
@ -86,7 +85,7 @@
|
|||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="update-form"
|
ref="updateFormRef"
|
||||||
:model="updateForm"
|
:model="updateForm"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
:hide-required-asterisk="true"
|
:hide-required-asterisk="true"
|
||||||
@ -119,109 +118,113 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
|
||||||
name: 'NotifyList',
|
|
||||||
data() {
|
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
|
||||||
return {
|
|
||||||
loading: false,
|
const loading = ref(false)
|
||||||
visible: false,
|
const visible = ref(false)
|
||||||
groupList: [],
|
const groupList = ref([])
|
||||||
groupForm: {
|
const groupForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
index: ''
|
index: ''
|
||||||
},
|
})
|
||||||
updateForm: {
|
const updateForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
index: ''
|
index: ''
|
||||||
},
|
})
|
||||||
rules: {
|
const rules = reactive({
|
||||||
'name': { required: true, message: '需输入分组名称', trigger: 'change' },
|
name: { required: true, message: '需输入分组名称', trigger: 'change' },
|
||||||
'index': { required: true, type: 'number', message: '需输入数字', trigger: 'change' }
|
index: { required: true, type: 'number', message: '需输入数字', trigger: 'change' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupFormRef = ref(null)
|
||||||
|
const updateFormRef = ref(null)
|
||||||
|
|
||||||
|
const hostGroupInfo = computed(() => {
|
||||||
|
const total = $store.hostList.length
|
||||||
|
const notGroupCount = $store.hostList.reduce((prev, next) => {
|
||||||
|
if (!next.group) prev++
|
||||||
|
return prev
|
||||||
|
}, 0)
|
||||||
|
return { total, notGroupCount }
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = computed(() => {
|
||||||
|
return groupList.value.map(item => {
|
||||||
|
const hosts = $store.hostList.reduce((prev, next) => {
|
||||||
|
if (next.group === item.id) {
|
||||||
|
prev.count++
|
||||||
|
prev.list.push(next)
|
||||||
}
|
}
|
||||||
}
|
return prev
|
||||||
},
|
}, { count: 0, list: [] })
|
||||||
computed: {
|
return { ...item, hosts }
|
||||||
hostGroupInfo() {
|
})
|
||||||
let total = this.$store.hostList.length
|
})
|
||||||
let notGroupCount = this.$store.hostList.reduce((prev, next) => {
|
|
||||||
if(!next.group) prev++
|
const getGroupList = () => {
|
||||||
return prev
|
loading.value = true
|
||||||
}, 0)
|
$api.getGroupList()
|
||||||
return { total, notGroupCount }
|
.then(({ data }) => {
|
||||||
},
|
groupList.value = data
|
||||||
list() {
|
groupForm.index = data.length
|
||||||
return this.groupList.map(item => {
|
})
|
||||||
let hosts = this.$store.hostList.reduce((prev, next) => {
|
.finally(() => loading.value = false)
|
||||||
if(next.group === item.id) {
|
|
||||||
prev.count++
|
|
||||||
prev.list.push(next)
|
|
||||||
}
|
|
||||||
return prev
|
|
||||||
}, { count: 0, list: [] })
|
|
||||||
return { ...item, hosts }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getGroupList()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getGroupList() {
|
|
||||||
this.loading = true
|
|
||||||
this.$api.getGroupList()
|
|
||||||
.then(({ data }) => {
|
|
||||||
this.groupList = data
|
|
||||||
this.groupForm.index = data.length
|
|
||||||
})
|
|
||||||
.finally(() => this.loading = false)
|
|
||||||
},
|
|
||||||
addGroup() {
|
|
||||||
let formRef = this.$refs['group-form']
|
|
||||||
formRef.validate()
|
|
||||||
.then(() => {
|
|
||||||
const { name, index } = this.groupForm
|
|
||||||
this.$api.addGroup({ name, index })
|
|
||||||
.then(() => {
|
|
||||||
this.$message.success('success')
|
|
||||||
this.groupForm = { name: '', index: '' }
|
|
||||||
this.$nextTick(() => formRef.resetFields())
|
|
||||||
this.getGroupList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleChange({ id, name, index }) {
|
|
||||||
this.updateForm = { id, name, index }
|
|
||||||
this.visible = true
|
|
||||||
},
|
|
||||||
updateGroup() {
|
|
||||||
let formRef = this.$refs['update-form']
|
|
||||||
formRef.validate()
|
|
||||||
.then(() => {
|
|
||||||
const { id, name, index } = this.updateForm
|
|
||||||
this.$api.updateGroup(id, { name, index })
|
|
||||||
.then(() => {
|
|
||||||
this.$message.success('success')
|
|
||||||
this.visible = false
|
|
||||||
this.getGroupList()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteGroup({ id, name }) {
|
|
||||||
this.$messageBox.confirm( `确认删除分组:${ name }`, 'Warning', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await this.$api.deleteGroup(id)
|
|
||||||
await this.$store.getHostList()
|
|
||||||
this.$message.success('success')
|
|
||||||
this.getGroupList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addGroup = () => {
|
||||||
|
groupFormRef.value.validate()
|
||||||
|
.then(() => {
|
||||||
|
const { name, index } = groupForm
|
||||||
|
$api.addGroup({ name, index })
|
||||||
|
.then(() => {
|
||||||
|
$message.success('success')
|
||||||
|
groupForm.name = ''
|
||||||
|
groupForm.index = ''
|
||||||
|
getGroupList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = ({ id, name, index }) => {
|
||||||
|
updateForm.id = id
|
||||||
|
updateForm.name = name
|
||||||
|
updateForm.index = index
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGroup = () => {
|
||||||
|
updateFormRef.value.validate()
|
||||||
|
.then(() => {
|
||||||
|
const { id, name, index } = updateForm
|
||||||
|
$api.updateGroup(id, { name, index })
|
||||||
|
.then(() => {
|
||||||
|
$message.success('success')
|
||||||
|
visible.value = false
|
||||||
|
getGroupList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteGroup = ({ id, name }) => {
|
||||||
|
$messageBox.confirm(`确认删除分组:${name}`, 'Warning', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await $api.deleteGroup(id)
|
||||||
|
await $store.getHostList()
|
||||||
|
$message.success('success')
|
||||||
|
getGroupList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getGroupList()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 提示 -->
|
|
||||||
<el-alert type="success" :closable="false">
|
<el-alert type="success" :closable="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span style="letter-spacing: 2px;"> Tips: 请添加邮箱并确保测试邮件通过 </span>
|
<span style="letter-spacing: 2px;"> Tips: 请添加邮箱并确保测试邮件通过 </span>
|
||||||
@ -21,43 +20,42 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, onMounted } from 'vue'
|
||||||
name: 'NotifyList',
|
import { getCurrentInstance } from 'vue'
|
||||||
data() {
|
|
||||||
return {
|
const { proxy: { $api, $message } } = getCurrentInstance()
|
||||||
notifyListLoading: false,
|
|
||||||
notifyList: []
|
const notifyListLoading = ref(false)
|
||||||
}
|
const notifyList = ref([])
|
||||||
},
|
|
||||||
mounted() {
|
const getNotifyList = (flag = true) => {
|
||||||
this.getNotifyList()
|
if (flag) notifyListLoading.value = true
|
||||||
},
|
$api.getNotifyList()
|
||||||
methods: {
|
.then(({ data }) => {
|
||||||
getNotifyList(flag = true) {
|
notifyList.value = data.map((item) => {
|
||||||
if(flag) this.notifyListLoading = true
|
item.loading = false
|
||||||
this.$api.getNotifyList()
|
return item
|
||||||
.then(({ data }) => {
|
})
|
||||||
this.notifyList = data.map((item) => {
|
})
|
||||||
item.loading = false
|
.finally(() => notifyListLoading.value = false)
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => this.notifyListLoading = false)
|
|
||||||
},
|
|
||||||
async handleChangeSw(row) {
|
|
||||||
row.loading = true
|
|
||||||
const { type, sw } = row
|
|
||||||
try {
|
|
||||||
await this.$api.updateNotifyList({ type, sw })
|
|
||||||
// if(this.userEmailList.length === 0) this.$message.warning('未配置邮箱, 此开关将不会生效')
|
|
||||||
} finally {
|
|
||||||
row.loading = true
|
|
||||||
}
|
|
||||||
this.getNotifyList(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChangeSw = async (row) => {
|
||||||
|
row.loading = true
|
||||||
|
const { type, sw } = row
|
||||||
|
try {
|
||||||
|
await $api.updateNotifyList({ type, sw })
|
||||||
|
// if (this.userEmailList.length === 0) $message.warning('未配置邮箱, 此开关将不会生效')
|
||||||
|
} finally {
|
||||||
|
row.loading = false
|
||||||
|
}
|
||||||
|
getNotifyList(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getNotifyList()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form
|
<el-form
|
||||||
ref="form"
|
ref="formRef"
|
||||||
class="password-form"
|
class="password-form"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
@ -40,46 +40,39 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, reactive, getCurrentInstance } from 'vue'
|
||||||
import { RSAEncrypt } from '@utils/index.js'
|
import { RSAEncrypt } from '@utils/index.js'
|
||||||
|
|
||||||
export default {
|
const { proxy: { $api, $message } } = getCurrentInstance()
|
||||||
name: 'UpdatePassword',
|
|
||||||
data() {
|
const loading = ref(false)
|
||||||
return {
|
const formRef = ref(null)
|
||||||
loading: false,
|
const formData = reactive({
|
||||||
formData: {
|
oldPwd: '',
|
||||||
oldPwd: '',
|
newPwd: '',
|
||||||
newPwd: '',
|
confirmPwd: ''
|
||||||
confirmPwd: ''
|
})
|
||||||
},
|
const rules = reactive({
|
||||||
rules: {
|
oldPwd: { required: true, message: '输入旧密码', trigger: 'change' },
|
||||||
oldPwd: { required: true, message: '输入旧密码', trigger: 'change' },
|
newPwd: { required: true, message: '输入新密码', trigger: 'change' },
|
||||||
newPwd: { required: true, message: '输入新密码', trigger: 'change' },
|
confirmPwd: { required: true, message: '输入确认密码', trigger: 'change' }
|
||||||
confirmPwd: { required: true, message: '输入确认密码', trigger: 'change' }
|
})
|
||||||
}
|
|
||||||
}
|
const handleUpdate = () => {
|
||||||
},
|
formRef.value.validate()
|
||||||
computed: {
|
.then(async () => {
|
||||||
formRef() {
|
let { oldPwd, newPwd, confirmPwd } = formData
|
||||||
return this.$refs['form']
|
if(newPwd !== confirmPwd) return $message.error({ center: true, message: '两次密码输入不一致' })
|
||||||
}
|
oldPwd = RSAEncrypt(oldPwd)
|
||||||
},
|
newPwd = RSAEncrypt(newPwd)
|
||||||
methods: {
|
let { msg } = await $api.updatePwd({ oldPwd, newPwd })
|
||||||
handleUpdate() {
|
$message({ type: 'success', center: true, message: msg })
|
||||||
this.formRef.validate()
|
formData.oldPwd = ''
|
||||||
.then(async () => {
|
formData.newPwd = ''
|
||||||
let { oldPwd, newPwd, confirmPwd } = this.formData
|
formData.confirmPwd = ''
|
||||||
if(newPwd !== confirmPwd) return this.$message.error({ center: true, message: '两次密码输入不一致' })
|
formRef.value.resetFields()
|
||||||
oldPwd = RSAEncrypt(oldPwd)
|
})
|
||||||
newPwd = RSAEncrypt(newPwd)
|
|
||||||
let { msg } = await this.$api.updatePwd({ oldPwd, newPwd })
|
|
||||||
this.$message({ type: 'success', center: true, message: msg })
|
|
||||||
this.formData = { oldPwd: '', newPwd: '', confirmPwd: '' }
|
|
||||||
this.formRef.resetFields()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -87,4 +80,4 @@ export default {
|
|||||||
.password-form {
|
.password-form {
|
||||||
width: 500px;
|
width: 500px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -15,36 +15,32 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||||
name: 'LoginRecord',
|
|
||||||
data() {
|
const { proxy: { $api, $tools } } = getCurrentInstance()
|
||||||
return {
|
|
||||||
loginRecordList: [],
|
const loginRecordList = ref([])
|
||||||
loading: false
|
const loading = ref(false)
|
||||||
}
|
|
||||||
},
|
const handleLookupLoginRecord = () => {
|
||||||
created() {
|
loading.value = true
|
||||||
this.handleLookupLoginRecord()
|
$api.getLoginRecord()
|
||||||
},
|
.then(({ data }) => {
|
||||||
methods: {
|
loginRecordList.value = data.map((item) => {
|
||||||
handleLookupLoginRecord() {
|
item.date = $tools.formatTimestamp(item.date)
|
||||||
this.loading = true
|
return item
|
||||||
this.$api.getLoginRecord()
|
})
|
||||||
.then(({ data }) => {
|
})
|
||||||
this.loginRecordList = data.map((item) => {
|
.finally(() => {
|
||||||
item.date = this.$tools.formatTimestamp(item.date)
|
loading.value = false
|
||||||
return item
|
})
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleLookupLoginRecord()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -19,53 +19,51 @@
|
|||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
</li>
|
</li>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
<div style="display: flex; justify-content: center;margin-top: 25px">
|
<div style="display: flex; justify-content: center; margin-top: 25px">
|
||||||
<el-button type="primary" @click="handleUpdateSort">
|
<el-button type="primary" @click="handleUpdateSort">
|
||||||
保存
|
保存
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||||
name: 'HostSort',
|
|
||||||
emits: ['update-list',],
|
const emit = defineEmits(['update-list'])
|
||||||
data() {
|
const { proxy: { $api, $message, $store } } = getCurrentInstance()
|
||||||
return {
|
|
||||||
targetIndex: 0,
|
const targetIndex = ref(0)
|
||||||
list: []
|
const list = ref([])
|
||||||
}
|
|
||||||
},
|
const dragstart = (index) => {
|
||||||
created() {
|
targetIndex.value = index
|
||||||
this.list = this.$store.hostList.map(({ name, host }) => ({ name, host }))
|
}
|
||||||
},
|
|
||||||
methods: {
|
const dragenter = (e, curIndex) => {
|
||||||
dragstart(index) {
|
e.preventDefault()
|
||||||
// console.log('拖动目标:', index)
|
if (targetIndex.value !== curIndex) {
|
||||||
this.targetIndex = index
|
let target = list.value.splice(targetIndex.value, 1)[0]
|
||||||
},
|
list.value.splice(curIndex, 0, target)
|
||||||
dragenter(e, curIndex) {
|
targetIndex.value = curIndex
|
||||||
e.preventDefault()
|
|
||||||
if (this.targetIndex !== curIndex) {
|
|
||||||
// console.log('拖动进入:', curIndex)
|
|
||||||
let target = this.list.splice(this.targetIndex, 1)[0]
|
|
||||||
this.list.splice(curIndex, 0, target)
|
|
||||||
this.targetIndex = curIndex // 每次拖动排序后重置目标元素下标
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dragover(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
},
|
|
||||||
handleUpdateSort() {
|
|
||||||
let { list } = this
|
|
||||||
this.$api.updateHostSort({ list })
|
|
||||||
.then(({ msg }) => {
|
|
||||||
this.$message({ type: 'success', center: true, message: msg })
|
|
||||||
this.$store.sortHostList(this.list)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dragover = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateSort = () => {
|
||||||
|
$api.updateHostSort({ list: list.value })
|
||||||
|
.then(({ msg }) => {
|
||||||
|
$message({ type: 'success', center: true, message: msg })
|
||||||
|
$store.sortHostList(list.value)
|
||||||
|
emit('update-list', list.value) // 触发自定义事件
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
list.value = $store.hostList.map(({ name, host }) => ({ name, host }))
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -82,18 +80,15 @@ export default {
|
|||||||
cursor: move;
|
cursor: move;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #595959;
|
color: #595959;
|
||||||
// width: 300px;
|
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
// background: #c8c8c8;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #000;
|
color: #000;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
line-height: 35px;
|
line-height: 35px;
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--el-box-shadow
|
box-shadow: var(--el-box-shadow);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
|
@ -6,10 +6,7 @@
|
|||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:close-on-press-escape="false"
|
:close-on-press-escape="false"
|
||||||
>
|
>
|
||||||
<el-tabs
|
<el-tabs style="height: 500px;" tab-position="left">
|
||||||
style="height: 500px;"
|
|
||||||
tab-position="left"
|
|
||||||
>
|
|
||||||
<el-tab-pane label="分组管理">
|
<el-tab-pane label="分组管理">
|
||||||
<Group />
|
<Group />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@ -17,7 +14,7 @@
|
|||||||
<Record />
|
<Record />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="主机排序" lazy>
|
<el-tab-pane label="主机排序" lazy>
|
||||||
<Sort @update-list="$emit('update-list')" />
|
<Sort @update-list="emitUpdateList" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="全局通知" lazy>
|
<el-tab-pane label="全局通知" lazy>
|
||||||
<NotifyList />
|
<NotifyList />
|
||||||
@ -32,8 +29,8 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
import NotifyList from './setting-tab/notify-list.vue'
|
import NotifyList from './setting-tab/notify-list.vue'
|
||||||
import EmailList from './setting-tab/email-list.vue'
|
import EmailList from './setting-tab/email-list.vue'
|
||||||
import Sort from './setting-tab/sort.vue'
|
import Sort from './setting-tab/sort.vue'
|
||||||
@ -41,37 +38,22 @@ import Record from './setting-tab/record.vue'
|
|||||||
import Group from './setting-tab/group.vue'
|
import Group from './setting-tab/group.vue'
|
||||||
import Password from './setting-tab/password.vue'
|
import Password from './setting-tab/password.vue'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: 'Setting',
|
show: {
|
||||||
components: {
|
required: true,
|
||||||
NotifyList,
|
type: Boolean
|
||||||
EmailList,
|
|
||||||
Sort,
|
|
||||||
Record,
|
|
||||||
Group,
|
|
||||||
Password
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
show: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['update:show', 'update-list',],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
visible: {
|
|
||||||
get() {
|
|
||||||
return this.show
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
this.$emit('update:show', newVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show', 'update-list'])
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (newVal) => emit('update:show', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
|
const emitUpdateList = () => {
|
||||||
|
emit('update-list')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="SSH连接"
|
title="SSH连接"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@closed="$nextTick(() => formRef.resetFields())"
|
@closed="clearFormInfo"
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="form"
|
ref="formRef"
|
||||||
:model="sshForm"
|
:model="sshForm"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
:hide-required-asterisk="true"
|
:hide-required-asterisk="true"
|
||||||
@ -55,7 +55,7 @@
|
|||||||
选择私钥...
|
选择私钥...
|
||||||
</el-button>
|
</el-button>
|
||||||
<input
|
<input
|
||||||
ref="privateKey"
|
ref="privateKeyRef"
|
||||||
type="file"
|
type="file"
|
||||||
name="privateKey"
|
name="privateKey"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
@ -86,121 +86,117 @@
|
|||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="handleSaveSSH">保存</el-button>
|
<el-button type="primary" @click="handleSaveSSH">保存</el-button>
|
||||||
<!-- <el-button type="primary" @click="handleSaveSSH">保存并连接</el-button> -->
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import $api from '@/api'
|
import { ref, reactive, computed, watch, getCurrentInstance, nextTick } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
|
||||||
import { randomStr, AESEncrypt, RSAEncrypt } from '@utils/index.js'
|
import { randomStr, AESEncrypt, RSAEncrypt } from '@utils/index.js'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: 'SSHForm',
|
show: {
|
||||||
props: {
|
required: true,
|
||||||
show: {
|
type: Boolean
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
tempHost: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
emits: ['update:show',],
|
tempHost: {
|
||||||
data() {
|
required: true,
|
||||||
return {
|
type: String
|
||||||
sshForm: {
|
|
||||||
host: '',
|
|
||||||
port: 22,
|
|
||||||
username: '',
|
|
||||||
type: 'privateKey',
|
|
||||||
password: '',
|
|
||||||
privateKey: '',
|
|
||||||
command: ''
|
|
||||||
},
|
|
||||||
defaultUsers: [
|
|
||||||
{ value: 'root' },
|
|
||||||
{ value: 'ubuntu' },
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
host: { required: true, message: '需输入主机', trigger: 'change' },
|
|
||||||
port: { required: true, message: '需输入端口', trigger: 'change' },
|
|
||||||
username: { required: true, message: '需输入用户名', trigger: 'change' },
|
|
||||||
type: { required: true },
|
|
||||||
password: { required: true, message: '需输入密码', trigger: 'change' },
|
|
||||||
privateKey: { required: true, message: '需输入密钥', trigger: 'change' },
|
|
||||||
command: { required: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
name: {
|
||||||
visible: {
|
required: true,
|
||||||
get() {
|
type: String
|
||||||
return this.show
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
this.$emit('update:show', newVal)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formRef() {
|
|
||||||
return this.$refs['form']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
tempHost: {
|
|
||||||
handler(newVal) {
|
|
||||||
this.sshForm.host = newVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClickUploadBtn() {
|
|
||||||
this.$refs['privateKey'].click()
|
|
||||||
},
|
|
||||||
handleSelectPrivateKeyFile(event) {
|
|
||||||
let file = event.target.files[0]
|
|
||||||
let reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
this.sshForm.privateKey = e.target.result
|
|
||||||
this.$refs['privateKey'].value = ''
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
},
|
|
||||||
handleSaveSSH() {
|
|
||||||
this.formRef.validate()
|
|
||||||
.then(async () => {
|
|
||||||
let randomKey = randomStr(16)
|
|
||||||
let formData = JSON.parse(JSON.stringify(this.sshForm))
|
|
||||||
// 加密传输
|
|
||||||
if(formData.password) formData.password = AESEncrypt(formData.password, randomKey)
|
|
||||||
if(formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, randomKey)
|
|
||||||
formData.randomKey = RSAEncrypt(randomKey)
|
|
||||||
await $api.updateSSH(formData)
|
|
||||||
this.$notification({
|
|
||||||
title: '保存成功',
|
|
||||||
message: '下次点击 [Web SSH] 可直接登录终端\n如无法登录请 [移除凭证] 后重新添加',
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
this.visible = false
|
|
||||||
// this.$message({ type: 'success', center: true, message: data })
|
|
||||||
// setTimeout(() => {
|
|
||||||
// window.open(`/terminal?host=${ this.tempHost }&name=${ this.name }`)
|
|
||||||
// }, 1000)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
userSearch(keyword, cb) {
|
|
||||||
let res = keyword
|
|
||||||
? this.defaultUsers.filter((item) => item.value.includes(keyword))
|
|
||||||
: this.defaultUsers
|
|
||||||
cb(res)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show'])
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const privateKeyRef = ref(null)
|
||||||
|
const sshForm = reactive({
|
||||||
|
host: '',
|
||||||
|
port: 22,
|
||||||
|
username: '',
|
||||||
|
type: 'privateKey',
|
||||||
|
password: '',
|
||||||
|
privateKey: '',
|
||||||
|
command: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultUsers = [
|
||||||
|
{ value: 'root' },
|
||||||
|
{ value: 'ubuntu' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
host: { required: true, message: '需输入主机', trigger: 'change' },
|
||||||
|
port: { required: true, message: '需输入端口', trigger: 'change' },
|
||||||
|
username: { required: true, message: '需输入用户名', trigger: 'change' },
|
||||||
|
type: { required: true },
|
||||||
|
password: { required: true, message: '需输入密码', trigger: 'change' },
|
||||||
|
privateKey: { required: true, message: '需输入密钥', trigger: 'change' },
|
||||||
|
command: { required: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const { proxy: { $api, $tools } } = getCurrentInstance()
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get() {
|
||||||
|
return props.show
|
||||||
|
},
|
||||||
|
set(newVal) {
|
||||||
|
emit('update:show', newVal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.tempHost, (newVal) => {
|
||||||
|
sshForm.host = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClickUploadBtn = () => {
|
||||||
|
privateKeyRef.value.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectPrivateKeyFile = (event) => {
|
||||||
|
let file = event.target.files[0]
|
||||||
|
let reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
sshForm.privateKey = e.target.result
|
||||||
|
privateKeyRef.value.value = ''
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveSSH = () => {
|
||||||
|
formRef.value.validate()
|
||||||
|
.then(async () => {
|
||||||
|
let randomKey = randomStr(16)
|
||||||
|
let formData = JSON.parse(JSON.stringify(sshForm))
|
||||||
|
// 加密传输
|
||||||
|
if (formData.password) formData.password = AESEncrypt(formData.password, randomKey)
|
||||||
|
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, randomKey)
|
||||||
|
formData.randomKey = RSAEncrypt(randomKey)
|
||||||
|
await $api.updateSSH(formData)
|
||||||
|
ElNotification({
|
||||||
|
title: '保存成功',
|
||||||
|
message: '下次点击 [Web SSH] 可直接登录终端\n如无法登录请 [移除凭证] 后重新添加',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
visible.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSearch = (keyword, cb) => {
|
||||||
|
let res = keyword
|
||||||
|
? defaultUsers.filter((item) => item.value.includes(keyword))
|
||||||
|
: defaultUsers
|
||||||
|
cb(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFormInfo = () => {
|
||||||
|
nextTick(() => formRef.value.resetFields())
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -45,118 +45,106 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
|
||||||
import { io } from 'socket.io-client'
|
import { io } from 'socket.io-client'
|
||||||
import HostForm from './components/host-form.vue'
|
import HostForm from './components/host-form.vue'
|
||||||
import Setting from './components/setting.vue'
|
import Setting from './components/setting.vue'
|
||||||
import HostCard from './components/host-card.vue'
|
import HostCard from './components/host-card.vue'
|
||||||
|
|
||||||
export default {
|
const { proxy: { $store, $api, $message, $notification, $router, $serviceURI } } = getCurrentInstance()
|
||||||
name: 'App',
|
|
||||||
components: {
|
const socket = ref(null)
|
||||||
HostCard,
|
const loading = ref(true)
|
||||||
HostForm,
|
const hostListStatus = ref([])
|
||||||
Setting
|
const updateHostData = ref(null)
|
||||||
},
|
const hostFormVisible = ref(false)
|
||||||
data() {
|
const settingVisible = ref(false)
|
||||||
return {
|
const hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
||||||
socket: null,
|
|
||||||
loading: true,
|
const handleLogout = () => {
|
||||||
hostListStatus: [],
|
$store.clearJwtToken()
|
||||||
updateHostData: null,
|
$message({ type: 'success', message: '已安全退出', center: true })
|
||||||
hostFormVisible: false,
|
$router.push('/login')
|
||||||
settingVisible: false,
|
}
|
||||||
hiddenIp: Number(localStorage.getItem('hiddenIp') || 0)
|
|
||||||
}
|
const getHostList = async () => {
|
||||||
},
|
try {
|
||||||
mounted() {
|
loading.value = true
|
||||||
this.getHostList()
|
await $store.getHostList()
|
||||||
},
|
connectIo()
|
||||||
beforeUnmount() {
|
} catch (err) {
|
||||||
this.socket?.close && this.socket.close()
|
loading.value = false
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleLogout() {
|
|
||||||
this.$store.clearJwtToken()
|
|
||||||
this.$message({ type: 'success', message: '已安全退出', center: true })
|
|
||||||
this.$router.push('/login')
|
|
||||||
},
|
|
||||||
async getHostList() {
|
|
||||||
try {
|
|
||||||
this.loading = true
|
|
||||||
await this.$store.getHostList()
|
|
||||||
this.connectIo()
|
|
||||||
} catch(err) {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
connectIo() {
|
|
||||||
let socket = io(this.$serviceURI, {
|
|
||||||
path: '/clients',
|
|
||||||
forceNew: true, // 强制新的实例
|
|
||||||
reconnectionDelay: 5000,
|
|
||||||
reconnectionAttempts: 2 // 每5s后尝试重新连接次数
|
|
||||||
})
|
|
||||||
this.socket = socket
|
|
||||||
socket.on('connect', () => {
|
|
||||||
let flag = 5
|
|
||||||
this.loading = false
|
|
||||||
console.log('clients websocket 已连接: ', socket.id)
|
|
||||||
let token = this.$store.token
|
|
||||||
socket.emit('init_clients_data', { token })
|
|
||||||
socket.on('clients_data', (data) => {
|
|
||||||
if((flag++ % 5) === 0) this.$store.getHostPing()
|
|
||||||
this.hostListStatus = this.$store.hostList.map(item => {
|
|
||||||
const { host } = item
|
|
||||||
if(data[host] === null) return { ...item }// 为null时表示该服务器断开连接
|
|
||||||
return Object.assign({}, item, data[host])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
socket.on('token_verify_fail', (message) => {
|
|
||||||
this.$notification({
|
|
||||||
title: '鉴权失败',
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
this.$router.push('/login')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
// this.$notification({
|
|
||||||
// title: 'server websocket error',
|
|
||||||
// message: '与服务器连接断开',
|
|
||||||
// type: 'error'
|
|
||||||
// })
|
|
||||||
console.error('clients websocket 连接断开')
|
|
||||||
})
|
|
||||||
socket.on('connect_error', (message) => {
|
|
||||||
this.loading = false
|
|
||||||
console.error('clients websocket 连接出错: ', message)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleUpdateList() {
|
|
||||||
this.socket.close && this.socket.close()
|
|
||||||
this.getHostList()
|
|
||||||
},
|
|
||||||
handleUpdateHost(defaultData) {
|
|
||||||
this.hostFormVisible = true
|
|
||||||
this.updateHostData = defaultData
|
|
||||||
},
|
|
||||||
handleHiddenIP() {
|
|
||||||
this.hiddenIp = this.hiddenIp ? 0 : 1
|
|
||||||
localStorage.setItem('hiddenIp', String(this.hiddenIp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectIo = () => {
|
||||||
|
let socketInstance = io($serviceURI, {
|
||||||
|
path: '/clients',
|
||||||
|
forceNew: true,
|
||||||
|
reconnectionDelay: 5000,
|
||||||
|
reconnectionAttempts: 2
|
||||||
|
})
|
||||||
|
socket.value = socketInstance
|
||||||
|
socketInstance.on('connect', () => {
|
||||||
|
let flag = 5
|
||||||
|
loading.value = false
|
||||||
|
console.log('clients websocket 已连接: ', socketInstance.id)
|
||||||
|
let token = $store.token
|
||||||
|
socketInstance.emit('init_clients_data', { token })
|
||||||
|
socketInstance.on('clients_data', (data) => {
|
||||||
|
if ((flag++ % 5) === 0) $store.getHostPing()
|
||||||
|
hostListStatus.value = $store.hostList.map(item => {
|
||||||
|
const { host } = item
|
||||||
|
if (data[host] === null) return { ...item }
|
||||||
|
return Object.assign({}, item, data[host])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
socketInstance.on('token_verify_fail', (message) => {
|
||||||
|
$notification({
|
||||||
|
title: '鉴权失败',
|
||||||
|
message,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
$router.push('/login')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
socketInstance.on('disconnect', () => {
|
||||||
|
console.error('clients websocket 连接断开')
|
||||||
|
})
|
||||||
|
socketInstance.on('connect_error', (message) => {
|
||||||
|
loading.value = false
|
||||||
|
console.error('clients websocket 连接出错: ', message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateList = () => {
|
||||||
|
if (socket.value) socket.value.close()
|
||||||
|
getHostList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateHost = (defaultData) => {
|
||||||
|
hostFormVisible.value = true
|
||||||
|
updateHostData.value = defaultData
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHiddenIP = () => {
|
||||||
|
hiddenIp.value = hiddenIp.value ? 0 : 1
|
||||||
|
localStorage.setItem('hiddenIp', String(hiddenIp.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getHostList()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (socket.value) socket.value.close()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$height:70px;
|
$height:70px;
|
||||||
header {
|
header {
|
||||||
// position: sticky;
|
|
||||||
// top: 0px;
|
|
||||||
// z-index: 1;
|
|
||||||
// background: rgba(255,255,255,0);
|
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
height: $height;
|
height: $height;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -194,4 +182,4 @@ footer {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -1,148 +1,98 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog v-model="visible" width="500px" :top="'30vh'" destroy-on-close :close-on-click-modal="false"
|
||||||
v-model="visible"
|
:close-on-press-escape="false" :show-close="false" center>
|
||||||
width="500px"
|
|
||||||
:top="'30vh'"
|
|
||||||
destroy-on-close
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
:close-on-press-escape="false"
|
|
||||||
:show-close="false"
|
|
||||||
center
|
|
||||||
>
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2 v-if="notKey" style="color: #f56c6c;"> Error </h2>
|
<h2 v-if="notKey" style="color: #f56c6c;"> Error </h2>
|
||||||
<h2 v-else style="color: #409eff;"> LOGIN </h2>
|
<h2 v-else style="color: #409eff;"> LOGIN </h2>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="notKey">
|
<div v-if="notKey">
|
||||||
<el-alert
|
<el-alert title="Error: 用于加密的公钥获取失败,请尝试重新启动或部署服务" type="error" show-icon />
|
||||||
title="Error: 用于加密的公钥获取失败,请尝试重新启动或部署服务"
|
|
||||||
type="error"
|
|
||||||
show-icon
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-form
|
<el-form ref="loginFormRefs" :model="loginForm" :rules="rules" :hide-required-asterisk="true" label-suffix=":"
|
||||||
ref="login-form"
|
label-width="90px">
|
||||||
:model="loginForm"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="90px"
|
|
||||||
>
|
|
||||||
<el-form-item prop="pwd" label="密码">
|
<el-form-item prop="pwd" label="密码">
|
||||||
<el-input
|
<el-input v-model.trim="loginForm.pwd" type="password" placeholder="Please input password" autocomplete="off"
|
||||||
v-model.trim="loginForm.pwd"
|
:trigger-on-focus="false" clearable show-password @keyup.enter="handleLogin" />
|
||||||
type="password"
|
|
||||||
placeholder="Please input password"
|
|
||||||
autocomplete="off"
|
|
||||||
:trigger-on-focus="false"
|
|
||||||
clearable
|
|
||||||
show-password
|
|
||||||
@keyup.enter="handleLogin"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item
|
<el-form-item v-show="false" prop="pwd" label="密码">
|
||||||
v-show="false"
|
|
||||||
prop="pwd"
|
|
||||||
label="密码"
|
|
||||||
>
|
|
||||||
<el-input v-model.trim="loginForm.pwd" />
|
<el-input v-model.trim="loginForm.pwd" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item
|
<el-form-item prop="jwtExpires" label="有效期">
|
||||||
prop="jwtExpires"
|
|
||||||
label="有效期"
|
|
||||||
>
|
|
||||||
<el-radio-group v-model="isSession" class="login-indate">
|
<el-radio-group v-model="isSession" class="login-indate">
|
||||||
<el-radio :value="true">一次性会话</el-radio>
|
<el-radio :value="true">一次性会话</el-radio>
|
||||||
<el-radio :value="false">自定义(小时)</el-radio>
|
<el-radio :value="false">自定义(小时)</el-radio>
|
||||||
<el-input-number
|
<el-input-number v-model="loginForm.jwtExpires" :disabled="isSession" placeholder="单位:小时" class="input"
|
||||||
v-model="loginForm.jwtExpires"
|
:min="1" :max="72" value-on-clear="min" size="small" controls-position="right" />
|
||||||
:disabled="isSession"
|
|
||||||
placeholder="单位:小时"
|
|
||||||
class="input"
|
|
||||||
:min="1"
|
|
||||||
:max="72"
|
|
||||||
value-on-clear="min"
|
|
||||||
size="small"
|
|
||||||
controls-position="right"
|
|
||||||
/>
|
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button
|
<el-button type="primary" :loading="loading" @click="handleLogin">登录</el-button>
|
||||||
type="primary"
|
|
||||||
:loading="loading"
|
|
||||||
@click="handleLogin"
|
|
||||||
>登录</el-button>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
|
||||||
import { RSAEncrypt } from '@utils/index.js'
|
import { RSAEncrypt } from '@utils/index.js'
|
||||||
|
// import { useRouter } from 'vue-router'
|
||||||
|
// import useStore from '@store/index'
|
||||||
|
|
||||||
export default {
|
// const router = useRouter()
|
||||||
name: 'App',
|
const { proxy: { $store, $api, $message, $router } } = getCurrentInstance()
|
||||||
data() {
|
|
||||||
return {
|
const loginFormRefs = ref(null)
|
||||||
isSession: true,
|
const isSession = ref(true)
|
||||||
visible: true,
|
const visible = ref(true)
|
||||||
notKey: false,
|
const notKey = ref(false)
|
||||||
loading: false,
|
const loading = ref(false)
|
||||||
loginForm: {
|
const loginForm = reactive({
|
||||||
pwd: '',
|
pwd: '',
|
||||||
jwtExpires: 8
|
jwtExpires: 8,
|
||||||
},
|
})
|
||||||
rules: {
|
const rules = reactive({
|
||||||
pwd: { required: true, message: '需输入密码', trigger: 'change' }
|
pwd: { required: true, message: '需输入密码', trigger: 'change' },
|
||||||
}
|
})
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
loginFormRefs.value.validate().then(() => {
|
||||||
|
let jwtExpires = isSession.value ? '12h' : `${loginForm.jwtExpires}h`
|
||||||
|
if (!isSession.value) {
|
||||||
|
localStorage.setItem('jwtExpires', loginForm.jwtExpires)
|
||||||
}
|
}
|
||||||
},
|
const ciphertext = RSAEncrypt(loginForm.pwd)
|
||||||
async created() {
|
if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
|
||||||
if(localStorage.getItem('jwtExpires')) this.loginForm.jwtExpires = Number(localStorage.getItem('jwtExpires'))
|
loading.value = true
|
||||||
// console.log(localStorage.getItem('jwtExpires'))
|
$api.login({ ciphertext, jwtExpires })
|
||||||
// 获取公钥
|
.then(({ data, msg }) => {
|
||||||
let { data } = await this.$api.getPubPem()
|
const { token } = data
|
||||||
if (!data) return (this.notKey = true)
|
$store.setJwtToken(token, isSession.value)
|
||||||
localStorage.setItem('publicKey', data)
|
$message.success({ message: msg || 'success', center: true })
|
||||||
},
|
$router.push('/')
|
||||||
methods: {
|
|
||||||
handleLogin() {
|
|
||||||
this.$refs['login-form'].validate().then(() => {
|
|
||||||
let { isSession, loginForm: { pwd, jwtExpires } } = this
|
|
||||||
if(isSession) jwtExpires = '12h' // 一次性token有效期12h,存储sessionStroage
|
|
||||||
else {
|
|
||||||
localStorage.setItem('jwtExpires', jwtExpires)
|
|
||||||
jwtExpires = `${ jwtExpires }h`
|
|
||||||
}
|
|
||||||
const ciphertext = RSAEncrypt(pwd)
|
|
||||||
if(ciphertext === -1) return this.$message.error({ message: '公钥加载失败', center: true })
|
|
||||||
this.loading = true
|
|
||||||
// console.log('加密后:', ciphertext)
|
|
||||||
this.$api.login({ ciphertext, jwtExpires })
|
|
||||||
.then(({ data, msg }) => {
|
|
||||||
let { token } = data
|
|
||||||
this.$store.setJwtToken(token, isSession)
|
|
||||||
this.$message.success({ message: msg || 'success', center: true })
|
|
||||||
this.$router.push('/')
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
.finally(() => {
|
||||||
}
|
loading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (localStorage.getItem('jwtExpires')) loginForm.jwtExpires = Number(localStorage.getItem('jwtExpires'))
|
||||||
|
const { data } = await $api.getPubPem()
|
||||||
|
if (!data) return (notKey.value = true)
|
||||||
|
localStorage.setItem('publicKey', data)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.login-indate {
|
.login-indate {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
margin-left: -25px;
|
margin-left: -25px;
|
||||||
// width: auto;
|
// width: auto;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="info-container" :style="{width: visible ? `250px` : 0}">
|
<div class="info-container" :style="{ width: visible ? `250px` : 0 }">
|
||||||
<header>
|
<header>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<img src="@/assets/logo-easynode.png" alt="logo">
|
<img src="@/assets/logo-easynode.png" alt="logo">
|
||||||
@ -12,12 +12,7 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
</header>
|
</header>
|
||||||
<el-divider class="first-divider" content-position="center">POSITION</el-divider>
|
<el-divider class="first-divider" content-position="center">POSITION</el-divider>
|
||||||
<el-descriptions
|
<el-descriptions class="margin-top" :column="1" size="small" border>
|
||||||
class="margin-top"
|
|
||||||
:column="1"
|
|
||||||
size="small"
|
|
||||||
border
|
|
||||||
>
|
|
||||||
<el-descriptions-item>
|
<el-descriptions-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="item-title">
|
<div class="item-title">
|
||||||
@ -48,24 +43,14 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<el-divider content-position="center">INDICATOR</el-divider>
|
<el-divider content-position="center">INDICATOR</el-divider>
|
||||||
<el-descriptions
|
<el-descriptions class="margin-top" :column="1" size="small" border>
|
||||||
class="margin-top"
|
|
||||||
:column="1"
|
|
||||||
size="small"
|
|
||||||
border
|
|
||||||
>
|
|
||||||
<el-descriptions-item>
|
<el-descriptions-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="item-title">
|
<div class="item-title">
|
||||||
CPU
|
CPU
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-progress
|
<el-progress :text-inside="true" :stroke-width="18" :percentage="cpuUsage" :color="handleColor(cpuUsage)" />
|
||||||
:text-inside="true"
|
|
||||||
:stroke-width="18"
|
|
||||||
:percentage="cpuUsage"
|
|
||||||
:color="handleColor(cpuUsage)"
|
|
||||||
/>
|
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item>
|
<el-descriptions-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
@ -73,12 +58,8 @@
|
|||||||
内存
|
内存
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-progress
|
<el-progress :text-inside="true" :stroke-width="18" :percentage="usedMemPercentage"
|
||||||
:text-inside="true"
|
:color="handleColor(usedMemPercentage)" />
|
||||||
:stroke-width="18"
|
|
||||||
:percentage="usedMemPercentage"
|
|
||||||
:color="handleColor(usedMemPercentage)"
|
|
||||||
/>
|
|
||||||
<div class="position-right">
|
<div class="position-right">
|
||||||
{{ $tools.toFixed(memInfo.usedMemMb / 1024) }}/{{ $tools.toFixed(memInfo.totalMemMb / 1024) }}G
|
{{ $tools.toFixed(memInfo.usedMemMb / 1024) }}/{{ $tools.toFixed(memInfo.totalMemMb / 1024) }}G
|
||||||
</div>
|
</div>
|
||||||
@ -89,12 +70,8 @@
|
|||||||
硬盘
|
硬盘
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-progress
|
<el-progress :text-inside="true" :stroke-width="18" :percentage="usedPercentage"
|
||||||
:text-inside="true"
|
:color="handleColor(usedPercentage)" />
|
||||||
:stroke-width="18"
|
|
||||||
:percentage="usedPercentage"
|
|
||||||
:color="handleColor(usedPercentage)"
|
|
||||||
/>
|
|
||||||
<div class="position-right">
|
<div class="position-right">
|
||||||
{{ driveInfo.usedGb || '--' }}/{{ driveInfo.totalGb || '--' }}G
|
{{ driveInfo.usedGb || '--' }}/{{ driveInfo.totalGb || '--' }}G
|
||||||
</div>
|
</div>
|
||||||
@ -119,12 +96,7 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<el-divider content-position="center">INFORMATION</el-divider>
|
<el-divider content-position="center">INFORMATION</el-divider>
|
||||||
<el-descriptions
|
<el-descriptions class="margin-top" :column="1" size="small" border>
|
||||||
class="margin-top"
|
|
||||||
:column="1"
|
|
||||||
size="small"
|
|
||||||
border
|
|
||||||
>
|
|
||||||
<el-descriptions-item>
|
<el-descriptions-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="item-title">
|
<div class="item-title">
|
||||||
@ -188,181 +160,167 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<el-divider content-position="center">FEATURE</el-divider>
|
<el-divider content-position="center">FEATURE</el-divider>
|
||||||
<el-button
|
<el-button :type="sftpStatus ? 'primary' : 'success'" style="display: block;width: 80%;margin: 30px auto;"
|
||||||
:type="sftpStatus ? 'primary' : 'success'"
|
@click="handleSftp">
|
||||||
style="display: block;width: 80%;margin: 30px auto;"
|
|
||||||
@click="handleSftp"
|
|
||||||
>
|
|
||||||
{{ sftpStatus ? '关闭SFTP' : '连接SFTP' }}
|
{{ sftpStatus ? '关闭SFTP' : '连接SFTP' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button :type="inputCommandStyle ? 'primary' : 'success'" style="display: block;width: 80%;margin: 30px auto;"
|
||||||
:type="inputCommandStatus ? 'primary' : 'success'"
|
@click="clickInputCommand">
|
||||||
style="display: block;width: 80%;margin: 30px auto;"
|
|
||||||
@click="clickInputCommand"
|
|
||||||
>
|
|
||||||
命令输入框
|
命令输入框
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
<script>
|
import { ref, reactive, onMounted, onBeforeUnmount, computed, getCurrentInstance } from 'vue'
|
||||||
import socketIo from 'socket.io-client'
|
import socketIo from 'socket.io-client'
|
||||||
|
|
||||||
export default {
|
const { proxy: { $router, $serviceURI, $message, $notification, $tools } } = getCurrentInstance()
|
||||||
name: 'InfoSide',
|
|
||||||
props: {
|
|
||||||
token: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
host: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['connect-sftp', 'click-input-command',],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
socket: null,
|
|
||||||
name: '',
|
|
||||||
hostData: null,
|
|
||||||
ping: 0,
|
|
||||||
pingTimer: null,
|
|
||||||
sftpStatus: false,
|
|
||||||
inputCommandStatus: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
ipInfo() {
|
|
||||||
return this.hostData?.ipInfo || {}
|
|
||||||
},
|
|
||||||
isError() {
|
|
||||||
return !Boolean(this.hostData?.osInfo) // 没获取系统信息默认未连接
|
|
||||||
},
|
|
||||||
cpuInfo() {
|
|
||||||
return this.hostData?.cpuInfo || {}
|
|
||||||
},
|
|
||||||
memInfo() {
|
|
||||||
return this.hostData?.memInfo || {}
|
|
||||||
},
|
|
||||||
osInfo() {
|
|
||||||
return this.hostData?.osInfo || {}
|
|
||||||
},
|
|
||||||
driveInfo() {
|
|
||||||
return this.hostData?.driveInfo || {}
|
|
||||||
},
|
|
||||||
netstatInfo() {
|
|
||||||
let { total: netTotal, ...netCards } = this.hostData?.netstatInfo || {}
|
|
||||||
return { netTotal, netCards: netCards || {} }
|
|
||||||
},
|
|
||||||
openedCount() {
|
|
||||||
return this.hostData?.openedCount || 0
|
|
||||||
},
|
|
||||||
cpuUsage() {
|
|
||||||
return Number(this.cpuInfo?.cpuUsage) || 0
|
|
||||||
},
|
|
||||||
usedMemPercentage() {
|
|
||||||
return Number(this.memInfo?.usedMemPercentage) || 0
|
|
||||||
},
|
|
||||||
usedPercentage() {
|
|
||||||
return Number(this.driveInfo?.usedPercentage) || 0
|
|
||||||
},
|
|
||||||
output() {
|
|
||||||
let outputMb = Number(this.netstatInfo.netTotal?.outputMb) || 0
|
|
||||||
if(outputMb >= 1 ) return `${ outputMb.toFixed(2) } MB/s`
|
|
||||||
return `${ (outputMb * 1024).toFixed(1) } KB/s`
|
|
||||||
},
|
|
||||||
input() {
|
|
||||||
let inputMb = Number(this.netstatInfo.netTotal?.inputMb) || 0
|
|
||||||
if(inputMb >= 1 ) return `${ inputMb.toFixed(2) } MB/s`
|
|
||||||
return `${ (inputMb * 1024).toFixed(1) } KB/s`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.name = this.$route.query.name || ''
|
|
||||||
if(!this.host || !this.name) return this.$message.error('参数错误')
|
|
||||||
this.connectIO()
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
this.socket && this.socket.close()
|
|
||||||
this.pingTimer && clearInterval(this.pingTimer)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleSftp() {
|
|
||||||
this.sftpStatus = !this.sftpStatus
|
|
||||||
this.$emit('connect-sftp', this.sftpStatus)
|
|
||||||
},
|
|
||||||
clickInputCommand() {
|
|
||||||
this.inputCommandStatus = true
|
|
||||||
this.$emit('click-input-command')
|
|
||||||
},
|
|
||||||
connectIO() {
|
|
||||||
let { host, token } = this
|
|
||||||
this.socket = socketIo(this.$serviceURI, {
|
|
||||||
path: '/host-status',
|
|
||||||
forceNew: true, // 强制新的实例
|
|
||||||
timeout: 5000,
|
|
||||||
reconnectionDelay: 3000,
|
|
||||||
reconnectionAttempts: 3
|
|
||||||
})
|
|
||||||
this.socket.on('connect', () => {
|
|
||||||
console.log('/host-status socket已连接:', this.socket.id)
|
|
||||||
this.socket.emit('init_host_data', { token, host })
|
|
||||||
this.getHostPing()
|
|
||||||
this.socket.on('host_data', (hostData) => {
|
|
||||||
if(!hostData) return this.hostData = null
|
|
||||||
this.hostData = hostData
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.socket.on('connect_error', (err) => {
|
const props = defineProps({
|
||||||
console.error('host status websocket 连接错误:', err)
|
token: {
|
||||||
this.$notification({
|
required: true,
|
||||||
title: '连接客户端失败(重连中...)',
|
type: String
|
||||||
message: '请检查客户端服务是否正常',
|
},
|
||||||
type: 'error'
|
host: {
|
||||||
})
|
required: true,
|
||||||
})
|
type: String
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
showInputCommand: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
this.socket.on('disconnect', () => {
|
const emit = defineEmits(['update:inputCommandStyle', 'connect-sftp', 'click-input-command'])
|
||||||
this.hostData = null
|
|
||||||
this.$notification({
|
const socket = ref(null)
|
||||||
title: '客户端连接主动断开(重连中...)',
|
const name = ref('')
|
||||||
message: '请检查客户端服务是否正常',
|
const hostData = ref(null)
|
||||||
type: 'error'
|
const ping = ref(0)
|
||||||
})
|
const pingTimer = ref(null)
|
||||||
})
|
const sftpStatus = ref(false)
|
||||||
},
|
|
||||||
async handleCopy() {
|
const ipInfo = computed(() => hostData.value?.ipInfo || {})
|
||||||
await navigator.clipboard.writeText(this.host)
|
const isError = computed(() => !Boolean(hostData.value?.osInfo))
|
||||||
this.$message.success({ message: 'success', center: true })
|
const cpuInfo = computed(() => hostData.value?.cpuInfo || {})
|
||||||
},
|
const memInfo = computed(() => hostData.value?.memInfo || {})
|
||||||
handleColor(num) {
|
const osInfo = computed(() => hostData.value?.osInfo || {})
|
||||||
if(num < 65) return '#8AE234'
|
const driveInfo = computed(() => hostData.value?.driveInfo || {})
|
||||||
if(num < 85) return '#FFD700'
|
const netstatInfo = computed(() => {
|
||||||
if(num < 90) return '#FFFF33'
|
let { total: netTotal, ...netCards } = hostData.value?.netstatInfo || {}
|
||||||
if(num <= 100) return '#FF3333'
|
return { netTotal, netCards: netCards || {} }
|
||||||
},
|
})
|
||||||
getHostPing() {
|
const openedCount = computed(() => hostData.value?.openedCount || 0)
|
||||||
this.pingTimer = setInterval(() => {
|
const cpuUsage = computed(() => Number(cpuInfo.value?.cpuUsage) || 0)
|
||||||
this.$tools.ping(`http://${ this.host }:22022`)
|
const usedMemPercentage = computed(() => Number(memInfo.value?.usedMemPercentage) || 0)
|
||||||
.then(res => {
|
const usedPercentage = computed(() => Number(driveInfo.value?.usedPercentage) || 0)
|
||||||
this.ping = res
|
const output = computed(() => {
|
||||||
if(!import.meta.env.DEV) {
|
let outputMb = Number(netstatInfo.value.netTotal?.outputMb) || 0
|
||||||
// console.clear()
|
if (outputMb >= 1) return `${outputMb.toFixed(2)} MB/s`
|
||||||
console.warn('Please tick \'Preserve Log\'')
|
return `${(outputMb * 1024).toFixed(1)} KB/s`
|
||||||
}
|
})
|
||||||
})
|
const input = computed(() => {
|
||||||
}, 3000)
|
let inputMb = Number(netstatInfo.value.netTotal?.inputMb) || 0
|
||||||
}
|
if (inputMb >= 1) return `${inputMb.toFixed(2)} MB/s`
|
||||||
|
return `${(inputMb * 1024).toFixed(1)} KB/s`
|
||||||
|
})
|
||||||
|
const inputCommandStyle = computed({
|
||||||
|
get: () => props.showInputCommand,
|
||||||
|
set: (val) => {
|
||||||
|
emit('update:inputCommandStyle', val)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSftp = () => {
|
||||||
|
sftpStatus.value = !sftpStatus.value
|
||||||
|
emit('connect-sftp', sftpStatus.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clickInputCommand = () => {
|
||||||
|
inputCommandStyle.value = true
|
||||||
|
emit('click-input-command')
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectIO = () => {
|
||||||
|
const { host, token } = props
|
||||||
|
socket.value = socketIo($serviceURI, {
|
||||||
|
path: '/host-status',
|
||||||
|
forceNew: true,
|
||||||
|
timeout: 5000,
|
||||||
|
reconnectionDelay: 3000,
|
||||||
|
reconnectionAttempts: 3
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('connect', () => {
|
||||||
|
console.log('/host-status socket已连接:', socket.value.id)
|
||||||
|
socket.value.emit('init_host_data', { token, host })
|
||||||
|
getHostPing()
|
||||||
|
socket.value.on('host_data', (data) => {
|
||||||
|
if (!data) return hostData.value = null
|
||||||
|
hostData.value = data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('connect_error', (err) => {
|
||||||
|
console.error('host status websocket 连接错误:', err)
|
||||||
|
$notification({
|
||||||
|
title: '连接客户端失败(重连中...)',
|
||||||
|
message: '请检查客户端服务是否正常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('disconnect', () => {
|
||||||
|
hostData.value = null
|
||||||
|
$notification({
|
||||||
|
title: '客户端连接主动断开(重连中...)',
|
||||||
|
message: '请检查客户端服务是否正常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
await navigator.clipboard.writeText(props.host)
|
||||||
|
$message.success({ message: 'success', center: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleColor = (num) => {
|
||||||
|
if (num < 65) return '#8AE234'
|
||||||
|
if (num < 85) return '#FFD700'
|
||||||
|
if (num < 90) return '#FFFF33'
|
||||||
|
if (num <= 100) return '#FF3333'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHostPing = () => {
|
||||||
|
pingTimer.value = setInterval(() => {
|
||||||
|
$tools.ping(`http://${props.host}:22022`)
|
||||||
|
.then(res => {
|
||||||
|
ping.value = res
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
console.warn('Please tick \'Preserve Log\'')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
name.value = $router.currentRoute.value.query.name || ''
|
||||||
|
if (!props.host || !name.value) return $message.error('参数错误')
|
||||||
|
connectIO()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
socket.value && socket.value.close()
|
||||||
|
pingTimer.value && clearInterval(pingTimer.value)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.info-container {
|
.info-container {
|
||||||
// min-width: 250px;
|
// min-width: 250px;
|
||||||
@ -372,6 +330,7 @@ export default {
|
|||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
background-color: #fff; //#E0E2EF;
|
background-color: #fff; //#E0E2EF;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -379,11 +338,13 @@ export default {
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表格中系统标识的title
|
// 表格中系统标识的title
|
||||||
.item-title {
|
.item-title {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -392,6 +353,7 @@ export default {
|
|||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
max-width: 30px;
|
max-width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.host-ping {
|
.host-ping {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -399,35 +361,43 @@ export default {
|
|||||||
background-color: #e8fff3;
|
background-color: #e8fff3;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分割线title
|
// 分割线title
|
||||||
:deep(.el-divider__text) {
|
:deep(.el-divider__text) {
|
||||||
color: #a0cfff;
|
color: #a0cfff;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分割线间距
|
// 分割线间距
|
||||||
:deep(.el-divider--horizontal) {
|
:deep(.el-divider--horizontal) {
|
||||||
margin: 28px 0 10px;
|
margin: 28px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.first-divider {
|
.first-divider {
|
||||||
margin: 15px 0 10px;
|
margin: 15px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表格
|
// 表格
|
||||||
:deep(.el-descriptions__table) {
|
:deep(.el-descriptions__table) {
|
||||||
tr {
|
tr {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.el-descriptions__label {
|
.el-descriptions__label {
|
||||||
min-width: 35px;
|
min-width: 35px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-descriptions__content {
|
.el-descriptions__content {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.el-progress {
|
.el-progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进度条右边参数定位
|
// 进度条右边参数定位
|
||||||
.position-right {
|
.position-right {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -436,36 +406,44 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进度条
|
// 进度条
|
||||||
:deep(.el-progress-bar__inner) {
|
:deep(.el-progress-bar__inner) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.el-progress-bar__innerText {
|
.el-progress-bar__innerText {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网络
|
// 网络
|
||||||
.netstat-info {
|
.netstat-info {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// justify-content: center;
|
// justify-content: center;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload {
|
.upload {
|
||||||
color: #CF8A20;
|
color: #CF8A20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download {
|
.download {
|
||||||
color: #67c23a;
|
color: #67c23a;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sftp-container">
|
<div class="sftp-container">
|
||||||
<div ref="adjust" class="adjust" />
|
<div ref="adjustRef" class="adjust" />
|
||||||
<section>
|
<section>
|
||||||
<div class="left box">
|
<div class="left box">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@ -62,10 +62,10 @@
|
|||||||
<img
|
<img
|
||||||
src="@/assets/image/system/upload.png"
|
src="@/assets/image/system/upload.png"
|
||||||
style=" width: 19px; height: 19px; "
|
style=" width: 19px; height: 19px; "
|
||||||
@click="$refs['upload_file'].click()"
|
@click="uploadFileRef.click()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="upload_file"
|
ref="uploadFileRef"
|
||||||
type="file"
|
type="file"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
multiple
|
multiple
|
||||||
@ -101,7 +101,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
v-if="fileList.length !== 0"
|
v-if="fileList.length !== 0"
|
||||||
ref="child-dir"
|
ref="childDirRef"
|
||||||
v-loading="childDirLoading"
|
v-loading="childDirLoading"
|
||||||
element-loading-text="加载中..."
|
element-loading-text="加载中..."
|
||||||
class="dir-list"
|
class="dir-list"
|
||||||
@ -132,7 +132,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
|
||||||
import socketIo from 'socket.io-client'
|
import socketIo from 'socket.io-client'
|
||||||
import CodeEdit from '@/components/code-edit/index.vue'
|
import CodeEdit from '@/components/code-edit/index.vue'
|
||||||
import { isDir, isFile, sortDirTree, downloadFile } from '@/utils'
|
import { isDir, isFile, sortDirTree, downloadFile } from '@/utils'
|
||||||
@ -142,408 +143,391 @@ import fileIcon from '@/assets/image/system/file.png'
|
|||||||
import unknowIcon from '@/assets/image/system/unknow.png'
|
import unknowIcon from '@/assets/image/system/unknow.png'
|
||||||
|
|
||||||
const { io } = socketIo
|
const { io } = socketIo
|
||||||
export default {
|
|
||||||
name: 'Sftp',
|
|
||||||
components: { CodeEdit },
|
|
||||||
props: {
|
|
||||||
token: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
host: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['resize',],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
visible: false,
|
|
||||||
originalCode: '',
|
|
||||||
filename: '',
|
|
||||||
filterKey: '',
|
|
||||||
socket: null,
|
|
||||||
icons: {
|
|
||||||
'-': fileIcon,
|
|
||||||
l: linkIcon,
|
|
||||||
d: dirIcon,
|
|
||||||
c: dirIcon,
|
|
||||||
p: unknowIcon,
|
|
||||||
s: unknowIcon,
|
|
||||||
b: unknowIcon
|
|
||||||
},
|
|
||||||
paths: ['/',],
|
|
||||||
rootLs: [],
|
|
||||||
childDir: [],
|
|
||||||
childDirLoading: false,
|
|
||||||
curTarget: null,
|
|
||||||
showFileProgress: false,
|
|
||||||
upFileProgress: 0,
|
|
||||||
curUploadFileName: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
curPath() {
|
|
||||||
return this.paths.join('/').replace(/\/{2,}/g, '/')
|
|
||||||
},
|
|
||||||
fileList() {
|
|
||||||
return this.childDir.filter(({ name }) => name.includes(this.filterKey))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.connectSftp()
|
|
||||||
this.adjustHeight()
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
this.socket && this.socket.close()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
connectSftp() {
|
|
||||||
let { host, token } = this
|
|
||||||
this.socket = io(this.$serviceURI, {
|
|
||||||
path: '/sftp',
|
|
||||||
forceNew: false, // 强制新的连接
|
|
||||||
reconnectionAttempts: 1 // 尝试重新连接次数
|
|
||||||
})
|
|
||||||
this.socket.on('connect', () => {
|
|
||||||
console.log('/sftp socket已连接:', this.socket.id)
|
|
||||||
this.listenSftp()
|
|
||||||
// 验证身份并连接终端
|
|
||||||
this.socket.emit('create', { host, token })
|
|
||||||
this.socket.on('root_ls', (tree) => {
|
|
||||||
// console.log(tree)
|
|
||||||
let temp = sortDirTree(tree).filter((item) => isDir(item.type)) // 只保留文件夹类型的文件
|
|
||||||
temp.unshift({ name: '/', type: 'd' })
|
|
||||||
this.rootLs = temp
|
|
||||||
})
|
|
||||||
this.socket.on('create_fail', (message) => {
|
|
||||||
// console.error(message)
|
|
||||||
this.$notification({
|
|
||||||
title: 'Sftp连接失败',
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.socket.on('token_verify_fail', () => {
|
|
||||||
this.$notification({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'token校验失败,需重新登录',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
// this.$router.push('/login')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.socket.on('disconnect', () => {
|
|
||||||
console.warn('sftp websocket 连接断开')
|
|
||||||
if(this.showFileProgress) {
|
|
||||||
this.$notification({
|
|
||||||
title: '上传失败',
|
|
||||||
message: '请检查socket服务是否正常',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
this.handleRefresh()
|
|
||||||
this.resetFileStatusFlag()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.socket.on('connect_error', (err) => {
|
|
||||||
console.error('sftp websocket 连接错误:', err)
|
|
||||||
this.$notification({
|
|
||||||
title: 'sftp连接失败',
|
|
||||||
message: '请检查socket服务是否正常',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// 这个方法连接socket只能调用一次,否则on回调会执行多次
|
|
||||||
listenSftp() {
|
|
||||||
this.socket.on('dir_ls', (dirLs) => {
|
|
||||||
// console.log('dir_ls: ', dirLs)
|
|
||||||
this.childDir = sortDirTree(dirLs)
|
|
||||||
this.childDirLoading = false
|
|
||||||
})
|
|
||||||
this.socket.on('not_exists_dir', (errMsg) => {
|
|
||||||
this.$message.error(errMsg)
|
|
||||||
this.childDirLoading = false
|
|
||||||
})
|
|
||||||
this.socket.on('rm_success', (res) => {
|
|
||||||
this.$message.success(res)
|
|
||||||
this.childDirLoading = false
|
|
||||||
this.handleRefresh()
|
|
||||||
})
|
|
||||||
// this.socket.on('down_dir_success', (res) => {
|
|
||||||
// console.log(res)
|
|
||||||
// this.$message.success(res)
|
|
||||||
// this.childDirLoading = false
|
|
||||||
// })
|
|
||||||
this.socket.on('down_file_success', (res) => {
|
|
||||||
const { buffer, name } = res
|
|
||||||
downloadFile({ buffer, name })
|
|
||||||
this.$message.success('success')
|
|
||||||
this.resetFileStatusFlag()
|
|
||||||
})
|
|
||||||
this.socket.on('preview_file_success', (res) => {
|
|
||||||
const { buffer, name } = res
|
|
||||||
console.log('preview_file: ', name, buffer)
|
|
||||||
// String.fromCharCode.apply(null, new Uint8Array(temp1))
|
|
||||||
this.originalCode = new TextDecoder().decode(buffer)
|
|
||||||
this.filename = name
|
|
||||||
this.visible = true
|
|
||||||
})
|
|
||||||
this.socket.on('sftp_error', (res) => {
|
|
||||||
console.log('操作失败:', res)
|
|
||||||
this.$message.error(res)
|
|
||||||
this.resetFileStatusFlag()
|
|
||||||
})
|
|
||||||
this.socket.on('up_file_progress', (res) => {
|
|
||||||
// console.log('上传进度:', res)
|
|
||||||
// 浏览器到服务端占比50%,服务端到服务器占比50%
|
|
||||||
let progress = Math.ceil(50 + (res / 2))
|
|
||||||
this.upFileProgress = progress > 100 ? 100 : progress
|
|
||||||
})
|
|
||||||
this.socket.on('down_file_progress', (res) => {
|
|
||||||
// console.log('下载进度:', res)
|
|
||||||
this.upFileProgress = res
|
|
||||||
})
|
|
||||||
},
|
|
||||||
openRootChild(item) {
|
|
||||||
const { name, type } = item
|
|
||||||
if(isDir(type)) {
|
|
||||||
this.childDirLoading = true
|
|
||||||
this.paths.length = 2
|
|
||||||
this.paths[1] = name
|
|
||||||
this.$refs['child-dir']?.scrollTo(0, 0)
|
|
||||||
this.openDir()
|
|
||||||
this.filterKey = '' // 移除搜索条件
|
|
||||||
}else {
|
|
||||||
console.log('暂不支持打开文件', name, type)
|
|
||||||
this.$message.warning(`暂不支持打开文件${ name } ${ type }`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openTarget(item) {
|
|
||||||
console.log(item)
|
|
||||||
const { name, type, size } = item
|
|
||||||
if(isDir(type)) {
|
|
||||||
this.paths.push(name)
|
|
||||||
this.$refs['child-dir']?.scrollTo(0, 0)
|
|
||||||
this.openDir()
|
|
||||||
} else if(isFile(type)) {
|
|
||||||
if(size/1024/1024 > 1) return this.$message.warning('暂不支持打开1M及以上文件, 请下载本地查看')
|
|
||||||
const path = this.getPath(name)
|
|
||||||
this.socket.emit('down_file', { path, name, size, target: 'preview' })
|
|
||||||
} else {
|
|
||||||
this.$message.warning(`暂不支持打开文件${ name } ${ type }`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSaveCode(code) {
|
|
||||||
// console.log('code: ', code)
|
|
||||||
let file = new TextEncoder('utf-8').encode(code)
|
|
||||||
let name = this.filename
|
|
||||||
const fullPath = this.getPath(name)
|
|
||||||
const targetPath = this.curPath
|
|
||||||
this.socket.emit('up_file', { targetPath, fullPath, name, file })
|
|
||||||
},
|
|
||||||
handleClosedCode() {
|
|
||||||
this.filename = ''
|
|
||||||
this.originalCode = ''
|
|
||||||
},
|
|
||||||
selectFile(item) {
|
|
||||||
this.curTarget = item
|
|
||||||
},
|
|
||||||
handleReturn() {
|
|
||||||
if(this.paths.length === 1) return
|
|
||||||
this.paths.pop()
|
|
||||||
this.openDir()
|
|
||||||
},
|
|
||||||
handleRefresh() {
|
|
||||||
this.openDir()
|
|
||||||
},
|
|
||||||
handleDownload() {
|
|
||||||
if(this.curTarget === null) return this.$message.warning('先选择一个文件')
|
|
||||||
const { name, size, type } = this.curTarget
|
|
||||||
if(isDir(type)) return this.$message.error('暂不支持下载文件夹')
|
|
||||||
this.$messageBox.confirm( `确认下载:${ name }`, 'Warning', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.childDirLoading = true
|
|
||||||
const path = this.getPath(name)
|
|
||||||
if(isDir(type)) {
|
|
||||||
// '暂不支持下载文件夹'
|
|
||||||
// this.socket.emit('down_dir', path)
|
|
||||||
}else if(isFile(type)) {
|
|
||||||
this.showFileProgress = true
|
|
||||||
this.socket.emit('down_file', { path, name, size, target: 'down' })
|
|
||||||
}else {
|
|
||||||
this.$message.error('不支持下载的文件类型')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleDelete() {
|
|
||||||
if(this.curTarget === null) return this.$message.warning('先选择一个文件(夹)')
|
|
||||||
const { name, type } = this.curTarget
|
|
||||||
this.$messageBox.confirm( `确认删除:${ name }`, 'Warning', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.childDirLoading = true
|
|
||||||
const path = this.getPath(name)
|
|
||||||
if(isDir(type)) {
|
|
||||||
this.socket.emit('rm_dir', path)
|
|
||||||
}else {
|
|
||||||
this.socket.emit('rm_file', path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async handleUpload(event) {
|
|
||||||
if(this.showFileProgress) return this.$message.warning('需等待当前任务完成')
|
|
||||||
let { files } = event.target
|
|
||||||
for(let file of files) {
|
|
||||||
console.log(file)
|
|
||||||
try {
|
|
||||||
await this.uploadFile(file)
|
|
||||||
} catch (error) {
|
|
||||||
this.$message.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.$refs['upload_file'].value = ''
|
|
||||||
},
|
|
||||||
uploadFile(file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if(!file) return reject('file is not defined')
|
|
||||||
if((file.size/1024/1024)> 1000) {
|
|
||||||
this.$message.warn('用网页传这么大文件你是认真的吗?')
|
|
||||||
}
|
|
||||||
let reader = new FileReader()
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
// console.log('buffer:', e.target.result)
|
|
||||||
const { name } = file
|
|
||||||
const fullPath = this.getPath(name)
|
|
||||||
const targetPath = this.curPath
|
|
||||||
this.curUploadFileName = name
|
|
||||||
this.socket.emit('create_cache_dir', { targetPath, name })
|
|
||||||
// 每次上传只监听一次,多次监听会导致回调重复执行
|
|
||||||
this.socket.once('create_cache_success', async () => {
|
|
||||||
let start = 0
|
|
||||||
let end = 0
|
|
||||||
let range = 1024 * 512 // 每段512KB
|
|
||||||
let size = file.size
|
|
||||||
let fileIndex = 0
|
|
||||||
let multipleFlag = false // 用于防止上一个文件失败导致多次执行once
|
|
||||||
try {
|
|
||||||
console.log('=========开始上传分片=========')
|
|
||||||
this.upFileProgress = 0
|
|
||||||
this.showFileProgress = true
|
|
||||||
this.childDirLoading = true
|
|
||||||
let totalSliceCount = Math.ceil(size / range)
|
|
||||||
while(end < size) {
|
|
||||||
fileIndex++
|
|
||||||
end += range
|
|
||||||
let sliceFile = file.slice(start, end)
|
|
||||||
start = end
|
|
||||||
await this.uploadSliceFile({ name, sliceFile, fileIndex })
|
|
||||||
// 浏览器到服务端占比50%,服务端到服务器占比50%
|
|
||||||
this.upFileProgress = parseInt((fileIndex / totalSliceCount * 100) / 2)
|
|
||||||
}
|
|
||||||
console.log('=========分片上传完成(等待服务端上传至客户端)=========')
|
|
||||||
this.socket.emit('up_file_slice_over', { name, fullPath, range, size })
|
|
||||||
this.socket.once('up_file_success', (res) => {
|
|
||||||
if(multipleFlag) return
|
|
||||||
console.log('=========服务端上传至客户端上传完成✔=========')
|
|
||||||
// console.log('up_file_success:', res)
|
|
||||||
// this.$message.success(res)
|
|
||||||
this.handleRefresh()
|
|
||||||
this.resetFileStatusFlag()
|
|
||||||
multipleFlag = true
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
this.socket.once('up_file_fail', (res) => {
|
|
||||||
if(multipleFlag) return
|
|
||||||
console.log('=========服务端上传至客户端上传失败❌=========')
|
|
||||||
// console.log('up_file_fail:', res)
|
|
||||||
this.$message.error(res)
|
|
||||||
this.handleRefresh()
|
|
||||||
this.resetFileStatusFlag()
|
|
||||||
multipleFlag = true
|
|
||||||
reject()
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
reject(err)
|
|
||||||
let errMsg = `上传失败, ${ err }`
|
|
||||||
console.error(errMsg)
|
|
||||||
this.$message.error(errMsg)
|
|
||||||
this.handleRefresh()
|
|
||||||
this.resetFileStatusFlag()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
reader.readAsArrayBuffer(file)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
resetFileStatusFlag() {
|
|
||||||
this.upFileProgress = 0
|
|
||||||
this.curUploadFileName = ''
|
|
||||||
this.showFileProgress = false
|
|
||||||
this.childDirLoading = false
|
|
||||||
},
|
|
||||||
uploadSliceFile(fileInfo) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.socket.emit('up_file_slice', fileInfo)
|
|
||||||
this.socket.once('up_file_slice_success', () => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
this.socket.once('up_file_slice_fail', () => {
|
|
||||||
reject('分片文件上传失败')
|
|
||||||
})
|
|
||||||
this.socket.once('not_exists_dir', (errMsg) => {
|
|
||||||
reject(errMsg)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
openDir() {
|
|
||||||
this.childDirLoading = true
|
|
||||||
this.curTarget = null
|
|
||||||
this.socket.emit('open_dir', this.curPath)
|
|
||||||
},
|
|
||||||
getPath(name = '') {
|
|
||||||
return this.curPath.length === 1 ? `/${ name }` : `${ this.curPath }/${ name }`
|
|
||||||
},
|
|
||||||
adjustHeight() {
|
|
||||||
let startAdjust = false
|
|
||||||
let timer = null
|
|
||||||
this.$nextTick(() => {
|
|
||||||
let sftpHeight = localStorage.getItem('sftpHeight')
|
|
||||||
if(sftpHeight) document.querySelector('.sftp-container').style.height = sftpHeight
|
|
||||||
else document.querySelector('.sftp-container').style.height = '33vh' // 默认占据页面高度1/3
|
|
||||||
|
|
||||||
this.$refs['adjust'].addEventListener('mousedown', () => {
|
const props = defineProps({
|
||||||
// console.log('开始调整')
|
token: {
|
||||||
startAdjust = true
|
required: true,
|
||||||
})
|
type: String
|
||||||
document.addEventListener('mousemove', (e) => {
|
},
|
||||||
if(!startAdjust) return
|
host: {
|
||||||
if(timer) clearTimeout(timer)
|
required: true,
|
||||||
timer = setTimeout(() => {
|
type: String
|
||||||
sftpHeight = `calc(100vh - ${ e.pageY }px)`
|
}
|
||||||
document.querySelector('.sftp-container').style.height = sftpHeight
|
})
|
||||||
this.$emit('resize')
|
|
||||||
})
|
const emit = defineEmits(['resize'])
|
||||||
})
|
|
||||||
document.addEventListener('mouseup', (e) => {
|
const { proxy: { $notification, $message, $messageBox, $serviceURI, $nextTick } } = getCurrentInstance()
|
||||||
if(!startAdjust) return
|
|
||||||
startAdjust = false
|
const visible = ref(false)
|
||||||
sftpHeight = `calc(100vh - ${ e.pageY }px)`
|
const originalCode = ref('')
|
||||||
localStorage.setItem('sftpHeight', sftpHeight)
|
const filename = ref('')
|
||||||
})
|
const filterKey = ref('')
|
||||||
|
const socket = ref(null)
|
||||||
|
const icons = {
|
||||||
|
'-': fileIcon,
|
||||||
|
l: linkIcon,
|
||||||
|
d: dirIcon,
|
||||||
|
c: dirIcon,
|
||||||
|
p: unknowIcon,
|
||||||
|
s: unknowIcon,
|
||||||
|
b: unknowIcon
|
||||||
|
}
|
||||||
|
const paths = ref(['/',])
|
||||||
|
const rootLs = ref([])
|
||||||
|
const childDir = ref([])
|
||||||
|
const childDirLoading = ref(false)
|
||||||
|
const curTarget = ref(null)
|
||||||
|
const showFileProgress = ref(false)
|
||||||
|
const upFileProgress = ref(0)
|
||||||
|
const curUploadFileName = ref('')
|
||||||
|
const adjustRef = ref(null)
|
||||||
|
const childDirRef = ref(null)
|
||||||
|
const uploadFileRef = ref(null)
|
||||||
|
|
||||||
|
const curPath = computed(() => paths.value.join('/').replace(/\/{2,}/g, '/'))
|
||||||
|
const fileList = computed(() => childDir.value.filter(({ name }) => name.includes(filterKey.value)))
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
connectSftp()
|
||||||
|
adjustHeight()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (socket.value) socket.value.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectSftp = () => {
|
||||||
|
socket.value = io($serviceURI, {
|
||||||
|
path: '/sftp',
|
||||||
|
forceNew: false,
|
||||||
|
reconnectionAttempts: 1
|
||||||
|
})
|
||||||
|
socket.value.on('connect', () => {
|
||||||
|
console.log('/sftp socket已连接:', socket.value.id)
|
||||||
|
listenSftp()
|
||||||
|
socket.value.emit('create', { host: props.host, token: props.token })
|
||||||
|
socket.value.on('root_ls', (tree) => {
|
||||||
|
let temp = sortDirTree(tree).filter((item) => isDir(item.type))
|
||||||
|
temp.unshift({ name: '/', type: 'd' })
|
||||||
|
rootLs.value = temp
|
||||||
|
})
|
||||||
|
socket.value.on('create_fail', (message) => {
|
||||||
|
$notification({
|
||||||
|
title: 'Sftp连接失败',
|
||||||
|
message,
|
||||||
|
type: 'error'
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
socket.value.on('token_verify_fail', () => {
|
||||||
|
$notification({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'token校验失败,需重新登录',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
socket.value.on('disconnect', () => {
|
||||||
|
console.warn('sftp websocket 连接断开')
|
||||||
|
if (showFileProgress.value) {
|
||||||
|
$notification({
|
||||||
|
title: '上传失败',
|
||||||
|
message: '请检查socket服务是否正常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
handleRefresh()
|
||||||
|
resetFileStatusFlag()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
socket.value.on('connect_error', (err) => {
|
||||||
|
console.error('sftp websocket 连接错误:', err)
|
||||||
|
$notification({
|
||||||
|
title: 'sftp连接失败',
|
||||||
|
message: '请检查socket服务是否正常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenSftp = () => {
|
||||||
|
socket.value.on('dir_ls', (dirLs) => {
|
||||||
|
childDir.value = sortDirTree(dirLs)
|
||||||
|
childDirLoading.value = false
|
||||||
|
})
|
||||||
|
socket.value.on('not_exists_dir', (errMsg) => {
|
||||||
|
$message.error(errMsg)
|
||||||
|
childDirLoading.value = false
|
||||||
|
})
|
||||||
|
socket.value.on('rm_success', (res) => {
|
||||||
|
$message.success(res)
|
||||||
|
childDirLoading.value = false
|
||||||
|
handleRefresh()
|
||||||
|
})
|
||||||
|
socket.value.on('down_file_success', (res) => {
|
||||||
|
const { buffer, name } = res
|
||||||
|
downloadFile({ buffer, name })
|
||||||
|
$message.success('success')
|
||||||
|
resetFileStatusFlag()
|
||||||
|
})
|
||||||
|
socket.value.on('preview_file_success', (res) => {
|
||||||
|
const { buffer, name } = res
|
||||||
|
originalCode.value = new TextDecoder().decode(buffer)
|
||||||
|
filename.value = name
|
||||||
|
visible.value = true
|
||||||
|
})
|
||||||
|
socket.value.on('sftp_error', (res) => {
|
||||||
|
$message.error(res)
|
||||||
|
resetFileStatusFlag()
|
||||||
|
})
|
||||||
|
socket.value.on('up_file_progress', (res) => {
|
||||||
|
let progress = Math.ceil(50 + (res / 2))
|
||||||
|
upFileProgress.value = progress > 100 ? 100 : progress
|
||||||
|
})
|
||||||
|
socket.value.on('down_file_progress', (res) => {
|
||||||
|
upFileProgress.value = res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openRootChild = (item) => {
|
||||||
|
const { name, type } = item
|
||||||
|
if (isDir(type)) {
|
||||||
|
childDirLoading.value = true
|
||||||
|
paths.value.length = 2
|
||||||
|
paths.value[1] = name
|
||||||
|
$nextTick(() => {
|
||||||
|
if (childDirRef.value) childDirRef.value.scrollTo(0, 0)
|
||||||
|
})
|
||||||
|
openDir()
|
||||||
|
filterKey.value = ''
|
||||||
|
} else {
|
||||||
|
$message.warning(`暂不支持打开文件${name} ${type}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openTarget = (item) => {
|
||||||
|
const { name, type, size } = item
|
||||||
|
if (isDir(type)) {
|
||||||
|
paths.value.push(name)
|
||||||
|
$nextTick(() => {
|
||||||
|
if (childDirRef.value) childDirRef.value.scrollTo(0, 0)
|
||||||
|
})
|
||||||
|
openDir()
|
||||||
|
} else if (isFile(type)) {
|
||||||
|
if (size / 1024 / 1024 > 1) return $message.warning('暂不支持打开1M及以上文件, 请下载本地查看')
|
||||||
|
const path = getPath(name)
|
||||||
|
socket.value.emit('down_file', { path, name, size, target: 'preview' })
|
||||||
|
} else {
|
||||||
|
$message.warning(`暂不支持打开文件${name} ${type}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveCode = (code) => {
|
||||||
|
let file = new TextEncoder('utf-8').encode(code)
|
||||||
|
let name = filename.value
|
||||||
|
const fullPath = getPath(name)
|
||||||
|
const targetPath = curPath.value
|
||||||
|
socket.value.emit('up_file', { targetPath, fullPath, name, file })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClosedCode = () => {
|
||||||
|
filename.value = ''
|
||||||
|
originalCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFile = (item) => {
|
||||||
|
curTarget.value = item
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReturn = () => {
|
||||||
|
if (paths.value.length === 1) return
|
||||||
|
paths.value.pop()
|
||||||
|
openDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
openDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (curTarget.value === null) return $message.warning('先选择一个文件')
|
||||||
|
const { name, size, type } = curTarget.value
|
||||||
|
if (isDir(type)) return $message.error('暂不支持下载文件夹')
|
||||||
|
$messageBox.confirm(`确认下载:${name}`, 'Warning', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
childDirLoading.value = true
|
||||||
|
const path = getPath(name)
|
||||||
|
if (isDir(type)) {
|
||||||
|
// '暂不支持下载文件夹'
|
||||||
|
} else if (isFile(type)) {
|
||||||
|
showFileProgress.value = true
|
||||||
|
socket.value.emit('down_file', { path, name, size, target: 'down' })
|
||||||
|
} else {
|
||||||
|
$message.error('不支持下载的文件类型')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (curTarget.value === null) return $message.warning('先选择一个文件(夹)')
|
||||||
|
const { name, type } = curTarget.value
|
||||||
|
$messageBox.confirm(`确认删除:${name}`, 'Warning', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
childDirLoading.value = true
|
||||||
|
const path = getPath(name)
|
||||||
|
if (isDir(type)) {
|
||||||
|
socket.value.emit('rm_dir', path)
|
||||||
|
} else {
|
||||||
|
socket.value.emit('rm_file', path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpload = async (event) => {
|
||||||
|
if (showFileProgress.value) return $message.warning('需等待当前任务完成')
|
||||||
|
let { files } = event.target
|
||||||
|
for (let file of files) {
|
||||||
|
try {
|
||||||
|
await uploadFile(file)
|
||||||
|
} catch (error) {
|
||||||
|
$message.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uploadFileRef.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFile = (file) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!file) return reject('file is not defined')
|
||||||
|
if ((file.size / 1024 / 1024) > 1000) {
|
||||||
|
$message.warn('用网页传这么大文件你是认真的吗?')
|
||||||
|
}
|
||||||
|
let reader = new
|
||||||
|
FileReader()
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const { name } = file
|
||||||
|
const fullPath = getPath(name)
|
||||||
|
const targetPath = curPath.value
|
||||||
|
curUploadFileName.value = name
|
||||||
|
socket.value.emit('create_cache_dir', { targetPath, name })
|
||||||
|
socket.value.once('create_cache_success', async () => {
|
||||||
|
let start = 0
|
||||||
|
let end = 0
|
||||||
|
const range = 1024 * 512 // 每段512KB
|
||||||
|
const size = file.size
|
||||||
|
let fileIndex = 0
|
||||||
|
let multipleFlag = false
|
||||||
|
try {
|
||||||
|
upFileProgress.value = 0
|
||||||
|
showFileProgress.value = true
|
||||||
|
childDirLoading.value = true
|
||||||
|
const totalSliceCount = Math.ceil(size / range)
|
||||||
|
while (end < size) {
|
||||||
|
fileIndex++
|
||||||
|
end += range
|
||||||
|
const sliceFile = file.slice(start, end)
|
||||||
|
start = end
|
||||||
|
await uploadSliceFile({ name, sliceFile, fileIndex })
|
||||||
|
upFileProgress.value = parseInt((fileIndex / totalSliceCount * 100) / 2)
|
||||||
|
}
|
||||||
|
socket.value.emit('up_file_slice_over', { name, fullPath, range, size })
|
||||||
|
socket.value.once('up_file_success', (res) => {
|
||||||
|
if (multipleFlag) return
|
||||||
|
handleRefresh()
|
||||||
|
resetFileStatusFlag()
|
||||||
|
multipleFlag = true
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
socket.value.once('up_file_fail', (res) => {
|
||||||
|
if (multipleFlag) return
|
||||||
|
$message.error(res)
|
||||||
|
handleRefresh()
|
||||||
|
resetFileStatusFlag()
|
||||||
|
multipleFlag = true
|
||||||
|
reject()
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
const errMsg = `上传失败, ${err}`
|
||||||
|
$message.error(errMsg)
|
||||||
|
handleRefresh()
|
||||||
|
resetFileStatusFlag()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFileStatusFlag = () => {
|
||||||
|
upFileProgress.value = 0
|
||||||
|
curUploadFileName.value = ''
|
||||||
|
showFileProgress.value = false
|
||||||
|
childDirLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadSliceFile = (fileInfo) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
socket.value.emit('up_file_slice', fileInfo)
|
||||||
|
socket.value.once('up_file_slice_success', () => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
socket.value.once('up_file_slice_fail', () => {
|
||||||
|
reject('分片文件上传失败')
|
||||||
|
})
|
||||||
|
socket.value.once('not_exists_dir', (errMsg) => {
|
||||||
|
reject(errMsg)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDir = () => {
|
||||||
|
childDirLoading.value = true
|
||||||
|
curTarget.value = null
|
||||||
|
socket.value.emit('open_dir', curPath.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPath = (name = '') => {
|
||||||
|
return curPath.value.length === 1 ? `/${name}` : `${curPath.value}/${name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustHeight = () => {
|
||||||
|
let startAdjust = false
|
||||||
|
let timer = null
|
||||||
|
$nextTick(() => {
|
||||||
|
let sftpHeight = localStorage.getItem('sftpHeight')
|
||||||
|
if (sftpHeight) document.querySelector('.sftp-container').style.height = sftpHeight
|
||||||
|
else document.querySelector('.sftp-container').style.height = '33vh'
|
||||||
|
|
||||||
|
adjustRef.value.addEventListener('mousedown', () => {
|
||||||
|
startAdjust = true
|
||||||
|
})
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!startAdjust) return
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
sftpHeight = `calc(100vh - ${e.pageY}px)`
|
||||||
|
document.querySelector('.sftp-container').style.height = sftpHeight
|
||||||
|
emit('resize')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
document.addEventListener('mouseup', (e) => {
|
||||||
|
if (!startAdjust) return
|
||||||
|
startAdjust = false
|
||||||
|
sftpHeight = `calc(100vh - ${e.pageY}px)`
|
||||||
|
localStorage.setItem('sftpHeight', sftpHeight)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.sftp-container {
|
.sftp-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -8,10 +8,11 @@
|
|||||||
粘贴
|
粘贴
|
||||||
</el-button> -->
|
</el-button> -->
|
||||||
</header>
|
</header>
|
||||||
<div ref="terminal" class="terminal-container" />
|
<div ref="terminalRefs" class="terminal-container" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
|
||||||
import { Terminal } from 'xterm'
|
import { Terminal } from 'xterm'
|
||||||
import 'xterm/css/xterm.css'
|
import 'xterm/css/xterm.css'
|
||||||
import { FitAddon } from 'xterm-addon-fit'
|
import { FitAddon } from 'xterm-addon-fit'
|
||||||
@ -21,242 +22,242 @@ import { WebLinksAddon } from 'xterm-addon-web-links'
|
|||||||
import socketIo from 'socket.io-client'
|
import socketIo from 'socket.io-client'
|
||||||
|
|
||||||
const { io } = socketIo
|
const { io } = socketIo
|
||||||
export default {
|
const { proxy: { $api, $serviceURI, $notification, $router, $messageBox } } = getCurrentInstance()
|
||||||
name: 'Terminal',
|
|
||||||
props: {
|
const props = defineProps({
|
||||||
token: {
|
token: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String
|
type: String
|
||||||
},
|
|
||||||
host: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
host: {
|
||||||
return {
|
required: true,
|
||||||
socket: null,
|
type: String
|
||||||
term: null,
|
|
||||||
command: '',
|
|
||||||
timer: null,
|
|
||||||
fitAddon: null,
|
|
||||||
searchBar: null,
|
|
||||||
isManual: false // 是否手动断开的socket连接
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async mounted() {
|
tabKey: {
|
||||||
this.createLocalTerminal()
|
required: true,
|
||||||
await this.getCommand()
|
type: String
|
||||||
this.connectIO()
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
// this.term.dispose() // 销毁终端
|
|
||||||
this.isManual = true
|
|
||||||
this.socket?.close() // 关闭socket连接
|
|
||||||
window.removeEventListener('resize', this.handleResize) // 移除resize监听
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async getCommand() {
|
|
||||||
let { data } = await this.$api.getCommand(this.host)
|
|
||||||
if(data) this.command = data
|
|
||||||
},
|
|
||||||
connectIO() {
|
|
||||||
let { host, token } = this
|
|
||||||
this.socket = io(this.$serviceURI, {
|
|
||||||
path: '/terminal',
|
|
||||||
forceNew: false, // 强制新的连接
|
|
||||||
reconnectionAttempts: 1 // 尝试重新连接次数
|
|
||||||
})
|
|
||||||
this.socket.on('connect', () => {
|
|
||||||
console.log('/terminal socket已连接:', this.socket.id)
|
|
||||||
// 验证身份并连接终端
|
|
||||||
this.socket.emit('create', { host, token })
|
|
||||||
this.socket.on('connect_success', () => {
|
|
||||||
this.onData() // 监听输入输出
|
|
||||||
this.socket.on('connect_terminal', () => {
|
|
||||||
this.onResize() // 自适应窗口(终端创建完成再适应)
|
|
||||||
this.onFindText() // 查找插件
|
|
||||||
this.onWebLinks() // link链接识别插件
|
|
||||||
if(this.command) this.socket.emit('input', this.command + '\n')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.socket.on('create_fail', (message) => {
|
|
||||||
console.error(message)
|
|
||||||
this.$notification({
|
|
||||||
title: '创建失败',
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.socket.on('token_verify_fail', () => {
|
|
||||||
this.$notification({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'token校验失败,请重新登录',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
this.$router.push('/login')
|
|
||||||
})
|
|
||||||
this.socket.on('connect_fail', (message) => {
|
|
||||||
console.error(message)
|
|
||||||
this.$notification({
|
|
||||||
title: '连接失败',
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.socket.on('disconnect', () => {
|
|
||||||
console.warn('terminal websocket 连接断开')
|
|
||||||
if(!this.isManual) this.reConnect()
|
|
||||||
})
|
|
||||||
this.socket.on('connect_error', (err) => {
|
|
||||||
console.error('terminal websocket 连接错误:', err)
|
|
||||||
this.$notification({
|
|
||||||
title: '终端连接失败',
|
|
||||||
message: '请检查socket服务是否正常',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
reConnect() {
|
|
||||||
this.socket.close && this.socket.close()
|
|
||||||
this.$messageBox.alert(
|
|
||||||
'<strong>终端连接断开</strong>',
|
|
||||||
'Error',
|
|
||||||
{
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
confirmButtonText: '刷新页面'
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
// this.fitAddon && this.fitAddon.dispose()
|
|
||||||
// this.term && this.term.dispose()
|
|
||||||
// this.connectIO()
|
|
||||||
location.reload()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createLocalTerminal() {
|
|
||||||
// https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/
|
|
||||||
let term = new Terminal({
|
|
||||||
rendererType: 'dom', // 渲染类型 canvas dom
|
|
||||||
bellStyle: 'sound',
|
|
||||||
// bellSound: './tip.mp3',
|
|
||||||
convertEol: true, // 启用时,光标将设置为下一行的开头
|
|
||||||
cursorBlink: true, // 光标闪烁
|
|
||||||
disableStdin: false, // 是否应禁用输入
|
|
||||||
fontSize: 18,
|
|
||||||
minimumContrastRatio: 7, // 文字对比度
|
|
||||||
theme: {
|
|
||||||
foreground: '#ECECEC', // 字体
|
|
||||||
background: '#000000', // 背景色
|
|
||||||
cursor: 'help', // 设置光标
|
|
||||||
selection: '#ff9900', // 选择文字颜色
|
|
||||||
lineHeight: 20
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.term = term
|
|
||||||
term.open(this.$refs['terminal'])
|
|
||||||
term.writeln('\x1b[1;32mWelcome to EasyNode terminal\x1b[0m.')
|
|
||||||
term.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.')
|
|
||||||
// 换行并输入起始符 $
|
|
||||||
// term.prompt = () => {
|
|
||||||
// term.write('\r\n\x1b[33m$ \x1b[0m ')
|
|
||||||
// }
|
|
||||||
term.focus()
|
|
||||||
this.onSelectionChange()
|
|
||||||
},
|
|
||||||
onResize() {
|
|
||||||
this.fitAddon = new FitAddon()
|
|
||||||
this.term.loadAddon(this.fitAddon)
|
|
||||||
this.fitAddon.fit()
|
|
||||||
let { rows, cols } = this.term
|
|
||||||
this.socket.emit('resize', { rows, cols }) // 首次fit完成后resize一次
|
|
||||||
window.addEventListener('resize', this.handleResize)
|
|
||||||
},
|
|
||||||
handleResize() {
|
|
||||||
if(this.timer) clearTimeout(this.timer)
|
|
||||||
this.timer = setTimeout(() => {
|
|
||||||
let temp = []
|
|
||||||
let panes= Array.from(document.getElementsByClassName('el-tab-pane'))
|
|
||||||
// 先block
|
|
||||||
panes.forEach((item, index) => {
|
|
||||||
temp[index] = item.style.display
|
|
||||||
item.style.display = 'block'
|
|
||||||
})
|
|
||||||
this.fitAddon?.fit() // 需要获取元素宽度(而element tab组件会display:none隐藏非当前tab)
|
|
||||||
// 还原
|
|
||||||
panes.forEach((item, index) => {
|
|
||||||
item.style.display = temp[index]
|
|
||||||
})
|
|
||||||
let { rows, cols } = this.term
|
|
||||||
// console.log('resize: ', { rows, cols })
|
|
||||||
this.socket?.emit('resize', { rows, cols })
|
|
||||||
}, 200)
|
|
||||||
},
|
|
||||||
onWebLinks() {
|
|
||||||
this.term.loadAddon(new WebLinksAddon())
|
|
||||||
},
|
|
||||||
onFindText() {
|
|
||||||
const searchAddon = new SearchAddon()
|
|
||||||
this.searchBar = new SearchBarAddon({ searchAddon })
|
|
||||||
this.term.loadAddon(searchAddon)
|
|
||||||
// searchAddon.findNext('SSH', { decorations: { activeMatchBackground: '#ff0000' } })
|
|
||||||
this.term.loadAddon(this.searchBar)
|
|
||||||
// this.searchBar.show()
|
|
||||||
},
|
|
||||||
onSelectionChange() {
|
|
||||||
this.term.onSelectionChange(() => {
|
|
||||||
let str = this.term.getSelection()
|
|
||||||
if(!str) return
|
|
||||||
const text = new Blob([str,], { type: 'text/plain' })
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const item = new ClipboardItem({
|
|
||||||
'text/plain': text
|
|
||||||
})
|
|
||||||
navigator.clipboard.write([item,])
|
|
||||||
// this.$message.success('copy success')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onData() {
|
|
||||||
this.socket.on('output', (str) => {
|
|
||||||
this.term.write(str)
|
|
||||||
})
|
|
||||||
this.term.onData((key) => {
|
|
||||||
let acsiiCode = key.codePointAt()
|
|
||||||
// console.log(acsiiCode)
|
|
||||||
if(acsiiCode === 22) return this.handlePaste() // ctrl + v
|
|
||||||
if(acsiiCode === 6) return this.searchBar.show() // ctrl + f
|
|
||||||
this.socket.emit('input', key)
|
|
||||||
})
|
|
||||||
// this.term.onKey(({ key }) => { // , domEvent
|
|
||||||
// // https://blog.csdn.net/weixin_30311605/article/details/98277379
|
|
||||||
// let acsiiCode = key.codePointAt() // codePointAt转换成ASCII码
|
|
||||||
// // console.log({ acsiiCode, domEvent })
|
|
||||||
// if(acsiiCode === 22) return this.handlePaste() // ctrl + v
|
|
||||||
// this.socket.emit('input', key)
|
|
||||||
// })
|
|
||||||
},
|
|
||||||
handleClear() {
|
|
||||||
this.term.clear()
|
|
||||||
},
|
|
||||||
async handlePaste() {
|
|
||||||
let str = await navigator.clipboard.readText()
|
|
||||||
// this.term.paste(str)
|
|
||||||
this.socket.emit('input', str)
|
|
||||||
this.term.focus()
|
|
||||||
},
|
|
||||||
// 供父组件调用
|
|
||||||
focusTab() {
|
|
||||||
this.term.blur()
|
|
||||||
setTimeout(() => {
|
|
||||||
this.term.focus()
|
|
||||||
}, 200)
|
|
||||||
},
|
|
||||||
// 供父组件调用
|
|
||||||
handleInputCommand(command) {
|
|
||||||
this.socket.emit('input', command)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const socket = ref(null)
|
||||||
|
const term = ref(null)
|
||||||
|
const command = ref('')
|
||||||
|
const timer = ref(null)
|
||||||
|
const fitAddon = ref(null)
|
||||||
|
const searchBar = ref(null)
|
||||||
|
const isManual = ref(false)
|
||||||
|
const terminalRefs = ref(null)
|
||||||
|
|
||||||
|
const tabKey = ref(props.tabKey)
|
||||||
|
|
||||||
|
const getCommand = async () => {
|
||||||
|
let { data } = await $api.getCommand(props.host)
|
||||||
|
if (data) command.value = data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectIO = () => {
|
||||||
|
const { host, token } = props
|
||||||
|
socket.value = io($serviceURI, {
|
||||||
|
path: '/terminal',
|
||||||
|
forceNew: false,
|
||||||
|
reconnectionAttempts: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('connect', () => {
|
||||||
|
console.log('/terminal socket已连接:', socket.value.id)
|
||||||
|
socket.value.emit('create', { host, token })
|
||||||
|
socket.value.on('connect_success', () => {
|
||||||
|
onData()
|
||||||
|
socket.value.on('connect_terminal', () => {
|
||||||
|
onResize()
|
||||||
|
onFindText()
|
||||||
|
onWebLinks()
|
||||||
|
if (command.value) socket.value.emit('input', command.value + '\n')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
socket.value.on('create_fail', (message) => {
|
||||||
|
console.error(message)
|
||||||
|
$notification({
|
||||||
|
title: '创建失败',
|
||||||
|
message,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
socket.value.on('token_verify_fail', () => {
|
||||||
|
$notification({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'token校验失败,请重新登录',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
$router.push('/login')
|
||||||
|
})
|
||||||
|
socket.value.on('connect_fail', (message) => {
|
||||||
|
console.error(message)
|
||||||
|
$notification({
|
||||||
|
title: '连接失败',
|
||||||
|
message,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('disconnect', () => {
|
||||||
|
console.warn('terminal websocket 连接断开')
|
||||||
|
if (!isManual.value) reConnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('connect_error', (err) => {
|
||||||
|
console.error('terminal websocket 连接错误:', err)
|
||||||
|
$notification({
|
||||||
|
title: '终端连接失败',
|
||||||
|
message: '请检查socket服务是否正常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const reConnect = () => {
|
||||||
|
socket.value.close && socket.value.close()
|
||||||
|
$messageBox.alert(
|
||||||
|
'<strong>终端连接断开</strong>',
|
||||||
|
'Error',
|
||||||
|
{
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
confirmButtonText: '刷新页面'
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createLocalTerminal = () => {
|
||||||
|
let terminal = new Terminal({
|
||||||
|
rendererType: 'dom',
|
||||||
|
bellStyle: 'sound',
|
||||||
|
convertEol: true,
|
||||||
|
cursorBlink: true,
|
||||||
|
disableStdin: false,
|
||||||
|
fontSize: 18,
|
||||||
|
minimumContrastRatio: 7,
|
||||||
|
theme: {
|
||||||
|
foreground: '#ECECEC',
|
||||||
|
background: '#000000',
|
||||||
|
cursor: 'help',
|
||||||
|
selection: '#ff9900',
|
||||||
|
lineHeight: 20
|
||||||
|
}
|
||||||
|
})
|
||||||
|
term.value = terminal
|
||||||
|
terminal.open(terminalRefs.value)
|
||||||
|
terminal.writeln('\x1b[1;32mWelcome to EasyNode terminal\x1b[0m.')
|
||||||
|
terminal.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.')
|
||||||
|
terminal.focus()
|
||||||
|
onSelectionChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
fitAddon.value = new FitAddon()
|
||||||
|
term.value.loadAddon(fitAddon.value)
|
||||||
|
fitAddon.value.fit()
|
||||||
|
let { rows, cols } = term.value
|
||||||
|
socket.value.emit('resize', { rows, cols })
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (timer.value) clearTimeout(timer.value)
|
||||||
|
timer.value = setTimeout(() => {
|
||||||
|
let temp = []
|
||||||
|
let panes = Array.from(document.getElementsByClassName('el-tab-pane'))
|
||||||
|
panes.forEach((item, index) => {
|
||||||
|
temp[index] = item.style.display
|
||||||
|
item.style.display = 'block'
|
||||||
|
})
|
||||||
|
fitAddon.value?.fit()
|
||||||
|
panes.forEach((item, index) => {
|
||||||
|
item.style.display = temp[index]
|
||||||
|
})
|
||||||
|
let { rows, cols } = term.value
|
||||||
|
socket.value?.emit('resize', { rows, cols })
|
||||||
|
focusTab()
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWebLinks = () => {
|
||||||
|
term.value.loadAddon(new WebLinksAddon())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFindText = () => {
|
||||||
|
const searchAddon = new SearchAddon()
|
||||||
|
searchBar.value = new SearchBarAddon({ searchAddon })
|
||||||
|
term.value.loadAddon(searchAddon)
|
||||||
|
term.value.loadAddon(searchBar.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectionChange = () => {
|
||||||
|
term.value.onSelectionChange(() => {
|
||||||
|
let str = term.value.getSelection()
|
||||||
|
if (!str) return
|
||||||
|
const text = new Blob([str], { type: 'text/plain' })
|
||||||
|
const item = new ClipboardItem({
|
||||||
|
'text/plain': text
|
||||||
|
})
|
||||||
|
navigator.clipboard.write([item])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onData = () => {
|
||||||
|
socket.value.on('output', (str) => {
|
||||||
|
term.value.write(str)
|
||||||
|
})
|
||||||
|
term.value.onData((key) => {
|
||||||
|
let acsiiCode = key.codePointAt()
|
||||||
|
if (acsiiCode === 22) return handlePaste()
|
||||||
|
if (acsiiCode === 6) return searchBar.value.show()
|
||||||
|
socket.value.emit('input', key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
term.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaste = async () => {
|
||||||
|
let str = await navigator.clipboard.readText()
|
||||||
|
socket.value.emit('input', str)
|
||||||
|
term.value.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusTab = () => {
|
||||||
|
term.value.blur()
|
||||||
|
setTimeout(() => {
|
||||||
|
term.value.focus()
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputCommand = (command) => {
|
||||||
|
socket.value.emit('input', command)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
createLocalTerminal()
|
||||||
|
await getCommand()
|
||||||
|
connectIO()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
isManual.value = true
|
||||||
|
socket.value?.close()
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focusTab,
|
||||||
|
handleResize,
|
||||||
|
handleInputCommand,
|
||||||
|
tabKey
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -266,11 +267,15 @@ header {
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-container {
|
.terminal-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
:deep(.xterm-viewport), :deep(.xterm-screen) {
|
|
||||||
width: 100%!important;
|
:deep(.xterm-viewport),
|
||||||
height: 100%!important;
|
:deep(.xterm-screen) {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
// 滚动条整体部分
|
// 滚动条整体部分
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
height: 5px;
|
height: 5px;
|
||||||
|
@ -1,195 +1,168 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<InfoSide
|
<InfoSide ref="infoSideRef" :showInputCommand.sync="showInputCommand" :token="token" :host="host" :visible="visible" @connect-sftp="connectSftp"
|
||||||
ref="info-side"
|
@click-input-command="clickInputCommand" />
|
||||||
:token="token"
|
|
||||||
:host="host"
|
|
||||||
:visible="visible"
|
|
||||||
@connect-sftp="connectSftp"
|
|
||||||
@click-input-command="clickInputComand"
|
|
||||||
/>
|
|
||||||
<section>
|
<section>
|
||||||
<div class="terminals">
|
<div class="terminals">
|
||||||
<el-button class="full-screen-button" type="success" @click="handleFullScreen">
|
<el-button class="full-screen-button" type="success" @click="handleFullScreen">
|
||||||
{{ isFullScreen ? '退出全屏' : '全屏' }}
|
{{ isFullScreen ? '退出全屏' : '全屏' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<div class="visible" @click="handleVisibleSidebar">
|
<div class="visible" @click="handleVisibleSidebar">
|
||||||
<svg-icon
|
<svg-icon name="icon-jiantou_zuoyouqiehuan" class="svg-icon" />
|
||||||
name="icon-jiantou_zuoyouqiehuan"
|
|
||||||
class="svg-icon"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<el-tabs
|
<el-tabs v-model="activeTab" type="border-card" addable tab-position="top" @tab-remove="removeTab"
|
||||||
v-model="activeTab"
|
@tab-change="tabChange" @tab-add="tabAdd">
|
||||||
type="border-card"
|
<el-tab-pane v-for="item in terminalTabs" :key="item.key" :label="item.title" :name="item.key"
|
||||||
addable
|
:closable="closable">
|
||||||
tab-position="top"
|
<TerminalTab ref="terminalTabRefs" :token="token" :host="host" :tab-key="item.key" />
|
||||||
@tab-remove="removeTab"
|
|
||||||
@tab-change="tabChange"
|
|
||||||
@tab-add="tabAdd"
|
|
||||||
>
|
|
||||||
<el-tab-pane
|
|
||||||
v-for="item in terminalTabs"
|
|
||||||
:key="item.key"
|
|
||||||
:label="item.title"
|
|
||||||
:name="item.key"
|
|
||||||
:closable="closable"
|
|
||||||
>
|
|
||||||
<TerminalTab :ref="item.key" :token="token" :host="host" />
|
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showSftp" class="sftp">
|
<div v-if="showSftp" class="sftp">
|
||||||
<SftpFooter
|
<SftpFooter :token="token" :host="host" @resize="resizeTerminal" />
|
||||||
:token="token"
|
|
||||||
:host="host"
|
|
||||||
@resize="resizeTerminal"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<InputCommand
|
<InputCommand v-model:show="showInputCommand" @input-command="handleInputCommand" />
|
||||||
v-model:show="showInputCommand"
|
|
||||||
@input-command="handleInputCommand"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onBeforeMount, getCurrentInstance, watch } from 'vue'
|
||||||
import TerminalTab from './components/terminal-tab.vue'
|
import TerminalTab from './components/terminal-tab.vue'
|
||||||
import InfoSide from './components/info-side.vue'
|
import InfoSide from './components/info-side.vue'
|
||||||
import SftpFooter from './components/sftp-footer.vue'
|
import SftpFooter from './components/sftp-footer.vue'
|
||||||
import InputCommand from '@/components/input-command/index.vue'
|
import InputCommand from '@/components/input-command/index.vue'
|
||||||
|
|
||||||
export default {
|
const { proxy: { $store, $router, $route, $nextTick } } = getCurrentInstance()
|
||||||
name: 'Terminals',
|
|
||||||
components: {
|
const name = ref('')
|
||||||
TerminalTab,
|
const host = ref('')
|
||||||
InfoSide,
|
const token = $store.token
|
||||||
SftpFooter,
|
const activeTab = ref('')
|
||||||
InputCommand
|
const terminalTabs = reactive([])
|
||||||
},
|
const isFullScreen = ref(false)
|
||||||
data() {
|
const timer = ref(null)
|
||||||
return {
|
const showSftp = ref(false)
|
||||||
name: '',
|
const showInputCommand = ref(false)
|
||||||
host: '',
|
const visible = ref(true)
|
||||||
token: this.$store.token,
|
const infoSideRef = ref(null)
|
||||||
activeTab: '',
|
const terminalTabRefs = ref([])
|
||||||
terminalTabs: [],
|
|
||||||
isFullScreen: false,
|
const closable = computed(() => terminalTabs.length > 1)
|
||||||
timer: null,
|
|
||||||
showSftp: false,
|
onBeforeMount(() => {
|
||||||
showInputCommand: false,
|
if (!token) return $router.push('login')
|
||||||
visible: true
|
let { host: routeHost, name: routeName } = $route.query
|
||||||
}
|
name.value = routeName
|
||||||
},
|
host.value = routeHost
|
||||||
computed: {
|
document.title = `${document.title}-${routeName}`
|
||||||
closable() {
|
let key = Date.now().toString()
|
||||||
return this.terminalTabs.length > 1
|
terminalTabs.push({ title: routeName, key })
|
||||||
}
|
activeTab.value = key
|
||||||
},
|
registryDbClick()
|
||||||
watch: {
|
})
|
||||||
showInputCommand(newVal) {
|
|
||||||
if(!newVal) this.$refs['info-side'].inputCommandStatus = false
|
// const windowBeforeUnload = () => {
|
||||||
}
|
// window.onbeforeunload = () => {
|
||||||
},
|
// return ''
|
||||||
created() {
|
// }
|
||||||
if (!this.token) return this.$router.push('login')
|
// }
|
||||||
let { host, name } = this.$route.query
|
|
||||||
this.name = name
|
const connectSftp = (flag) => {
|
||||||
this.host = host
|
showSftp.value = flag
|
||||||
document.title = `${ document.title }-${ name }`
|
resizeTerminal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickInputCommand = () => {
|
||||||
|
showInputCommand.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabAdd = () => {
|
||||||
|
if (timer.value) clearTimeout(timer.value)
|
||||||
|
timer.value = setTimeout(() => {
|
||||||
|
let title = name.value
|
||||||
let key = Date.now().toString()
|
let key = Date.now().toString()
|
||||||
this.terminalTabs.push({ title: name, key })
|
terminalTabs.push({ title, key })
|
||||||
this.activeTab = key
|
activeTab.value = key
|
||||||
this.registryDbClick()
|
tabChange(key)
|
||||||
},
|
registryDbClick()
|
||||||
// mounted() {
|
}, 200)
|
||||||
// window.onbeforeunload = () => {
|
}
|
||||||
// return ''
|
|
||||||
// }
|
const removeTab = (removeKey) => {
|
||||||
// },
|
let idx = terminalTabs.findIndex(({ key }) => removeKey === key)
|
||||||
methods: {
|
terminalTabs.splice(idx, 1)
|
||||||
connectSftp(flag) {
|
if (removeKey !== activeTab.value) return
|
||||||
this.showSftp = flag
|
activeTab.value = terminalTabs[0].key
|
||||||
this.resizeTerminal()
|
}
|
||||||
},
|
|
||||||
clickInputComand() {
|
const tabChange = async (key) => {
|
||||||
this.showInputCommand = true
|
await $nextTick()
|
||||||
},
|
const curTabTerminal = terminalTabRefs.value.find(({ tabKey }) => key === tabKey)
|
||||||
tabAdd() {
|
curTabTerminal?.focusTab()
|
||||||
if(this.timer) clearTimeout(this.timer)
|
}
|
||||||
this.timer = setTimeout(() => {
|
|
||||||
let { name } = this
|
const handleFullScreen = () => {
|
||||||
let title = name
|
if (isFullScreen.value) document.exitFullscreen()
|
||||||
let key = Date.now().toString()
|
else document.getElementsByClassName('terminals')[0].requestFullscreen()
|
||||||
this.terminalTabs.push({ title, key })
|
isFullScreen.value = !isFullScreen.value
|
||||||
this.activeTab = key
|
}
|
||||||
this.registryDbClick()
|
|
||||||
}, 200)
|
const registryDbClick = () => {
|
||||||
},
|
$nextTick(() => {
|
||||||
removeTab(removeKey) {
|
let tabItems = Array.from(document.getElementsByClassName('el-tabs__item'))
|
||||||
let idx = this.terminalTabs.findIndex(({ key }) => removeKey === key)
|
tabItems.forEach(item => {
|
||||||
this.terminalTabs.splice(idx, 1)
|
item.removeEventListener('dblclick', handleDblclick)
|
||||||
if(removeKey !== this.activeTab) return
|
item.addEventListener('dblclick', handleDblclick)
|
||||||
this.activeTab = this.terminalTabs[0].key
|
})
|
||||||
},
|
})
|
||||||
tabChange(key) {
|
}
|
||||||
this.$refs[key][0].focusTab()
|
|
||||||
},
|
const handleDblclick = (e) => {
|
||||||
handleFullScreen() {
|
if (terminalTabs.length > 1) {
|
||||||
if(this.isFullScreen) document.exitFullscreen()
|
let key = e.target.id.substring(4)
|
||||||
else document.getElementsByClassName('terminals')[0].requestFullscreen()
|
// console.log('dblclick', key)
|
||||||
this.isFullScreen = !this.isFullScreen
|
removeTab(key)
|
||||||
},
|
|
||||||
registryDbClick() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
let tabItems = Array.from(document.getElementsByClassName('el-tabs__item'))
|
|
||||||
tabItems.forEach(item => {
|
|
||||||
item.removeEventListener('dblclick', this.handleDblclick)
|
|
||||||
item.addEventListener('dblclick', this.handleDblclick)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleDblclick(e) {
|
|
||||||
if(this.terminalTabs.length > 1) {
|
|
||||||
let key = e.target.id.substring(4)
|
|
||||||
// console.log('dblclick', key)
|
|
||||||
this.removeTab(key)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleVisibleSidebar() {
|
|
||||||
this.visible = !this.visible
|
|
||||||
this.resizeTerminal()
|
|
||||||
},
|
|
||||||
resizeTerminal() {
|
|
||||||
let terminals = this.$refs
|
|
||||||
for(let terminal in terminals) {
|
|
||||||
const { handleResize } = this.$refs[terminal][0] || {}
|
|
||||||
handleResize && handleResize()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleInputCommand(command) {
|
|
||||||
// console.log(command)
|
|
||||||
this.$refs[this.activeTab][0].handleInputCommand(`${ command }\n`)
|
|
||||||
this.showInputCommand = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleVisibleSidebar = () => {
|
||||||
|
visible.value = !visible.value
|
||||||
|
resizeTerminal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeTerminal = () => {
|
||||||
|
for (let terminalTabRef of terminalTabRefs.value) {
|
||||||
|
const { handleResize } = terminalTabRef || {}
|
||||||
|
handleResize && handleResize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputCommand = async (command) => {
|
||||||
|
const curTabTerminal = terminalTabRefs.value.find(({ tabKey }) => activeTab.value === tabKey)
|
||||||
|
await $nextTick()
|
||||||
|
curTabTerminal?.focusTab()
|
||||||
|
curTabTerminal.handleInputCommand(`${command}\n`)
|
||||||
|
showInputCommand.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
section {
|
section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: calc(100vw - 250px); // 减去左边栏
|
width: calc(100vw - 250px); // 减去左边栏
|
||||||
|
|
||||||
.terminals {
|
.terminals {
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.full-screen-button {
|
.full-screen-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@ -197,9 +170,11 @@ export default {
|
|||||||
z-index: 99999;
|
z-index: 99999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sftp {
|
.sftp {
|
||||||
border: 1px solid rgb(236, 215, 187);
|
border: 1px solid rgb(236, 215, 187);
|
||||||
}
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
@ -207,6 +182,7 @@ export default {
|
|||||||
left: 5px;
|
left: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
@ -219,47 +195,46 @@ export default {
|
|||||||
.el-tabs {
|
.el-tabs {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs--border-card>.el-tabs__content {
|
.el-tabs--border-card>.el-tabs__content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__header {
|
.el-tabs__header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__nav-scroll {
|
.el-tabs__nav-scroll {
|
||||||
.el-tabs__nav {
|
.el-tabs__nav {
|
||||||
padding-left: 60px;
|
// padding-left: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__new-tab {
|
.el-tabs__new-tab {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 18px;
|
left: 18px;
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
z-index: 98;
|
z-index: 98;
|
||||||
// &::before {
|
|
||||||
// font-family: iconfont;
|
|
||||||
// content: '\eb0d';
|
|
||||||
// font-size: 12px;
|
|
||||||
// font-size: 18px;
|
|
||||||
// position: absolute;
|
|
||||||
// left: -28px;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
.el-tabs--border-card {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// .el-tabs--border-card {
|
||||||
|
// height: 100%;
|
||||||
|
// overflow: hidden;
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
// }
|
||||||
|
|
||||||
.el-tabs__content {
|
.el-tabs__content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-icon.is-icon-close {
|
.el-icon.is-icon-close {
|
||||||
position: absolute;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
top: 2px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
Loading…
x
Reference in New Issue
Block a user