optimization

This commit is contained in:
chaoszhu 2024-07-16 16:24:17 +08:00
parent f2fe091d2d
commit 62478abf95
22 changed files with 1845 additions and 2099 deletions

View File

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

View File

@ -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: '未知',

View File

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

View File

@ -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({
export default {
name: 'InputCommand',
props: {
show: { show: {
required: true, required: true,
type: Boolean type: Boolean
} }
}, })
emits: ['update:show', 'closed', 'input-command',],
data() { const emit = defineEmits(['update:show', 'closed', 'input-command'])
return {
command: '' const command = ref('')
}
}, const visible = computed({
computed: {
visible: {
get() { get() {
return this.show return props.show
}, },
set(newVal) { set(newVal) {
this.$emit('update:show', newVal) emit('update:show', newVal)
}
}
},
methods: {
handleSave() {
this.$emit('input-command', this.command)
}
} }
})
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;

View File

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

View File

@ -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
},
props: {
hostInfo: { hostInfo: {
required: true, required: true,
type: Object type: Object
}, },
hiddenIp: { hiddenIp: {
required: true, required: true,
type: [Number, Boolean,] type: [Number, Boolean]
} }
}, })
emits: ['update-list', 'update-host',],
data() { const emit = defineEmits(['update-list', 'update-host'])
return {
sshFormVisible: false, const sshFormVisible = ref(false)
tempHost: '' const tempHost = ref('')
}
}, const hostIp = computed(() => {
computed: { let ip = props.hostInfo?.ipInfo?.query || props.hostInfo?.host || '--'
hostIp() {
let ip = this.ipInfo?.query || this.host || '--'
try { 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('.') let formatIp = ip.replace(/\d/g, '*').split('.').map((item) => item.padStart(3, '*')).join('.')
return this.hiddenIp ? formatIp : ip return props.hiddenIp ? formatIp : ip
} catch (error) { } catch (error) {
return ip return ip
} }
}, })
host() {
return this.hostInfo?.host const host = computed(() => props.hostInfo?.host)
}, const name = computed(() => props.hostInfo?.name)
name() { const ping = computed(() => props.hostInfo?.ping || '')
return this.hostInfo?.name const expiredTime = computed(() => $tools.formatTimestamp(props.hostInfo?.expired, 'date'))
}, const consoleUrl = computed(() => props.hostInfo?.consoleUrl)
ping() { const ipInfo = computed(() => props.hostInfo?.ipInfo || {})
return this.hostInfo?.ping || '' const isError = computed(() => !Boolean(props.hostInfo?.osInfo))
}, const cpuInfo = computed(() => props.hostInfo?.cpuInfo || {})
expiredTime() { const memInfo = computed(() => props.hostInfo?.memInfo || {})
return this.$tools.formatTimestamp(this.hostInfo?.expired, 'date') const osInfo = computed(() => props.hostInfo?.osInfo || {})
}, const driveInfo = computed(() => props.hostInfo?.driveInfo || {})
consoleUrl() { const netstatInfo = computed(() => {
return this.hostInfo?.consoleUrl let { total: netTotal, ...netCards } = props.hostInfo?.netstatInfo || {}
},
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 || {} } return { netTotal, netCards: netCards || {} }
}, })
openedCount() { const openedCount = computed(() => props.hostInfo?.openedCount || 0)
return this.hostInfo?.openedCount || 0
} const setColor = (num) => {
},
mounted() {
// if (data?.message === 'private range') {
// data.country = ''
// data.city = ''
// }
},
methods: {
setColor(num){
num = Number(num) num = Number(num)
return num ? (num < 80 ? '#595959' : ((num >= 80 && num < 90) ? '#FF6600' : '#FF0000')) : '#595959' return num ? (num < 80 ? '#595959' : (num >= 80 && num < 90 ? '#FF6600' : '#FF0000')) : '#595959'
}, }
handleUpdate() {
let { name, host, hostInfo: { expired, expiredNotify, group, consoleUrl, remark } } = this const handleUpdate = () => {
this.$emit('update-host', { name, host, expired, expiredNotify, group, consoleUrl, remark }) let { expired, expiredNotify, group, consoleUrl, remark } = props.hostInfo
}, emit('update-host', { name: name.value, host: host.value, expired, expiredNotify, group, consoleUrl, remark })
handleToConsole() { }
window.open(this.consoleUrl)
}, const handleToConsole = () => {
async handleSSH() { window.open(consoleUrl.value)
let { host, name } = this }
let { data } = await this.$api.existSSH(host)
console.log('是否存在凭证:', data) const handleSSH = async () => {
if (data) return window.open(`/terminal?host=${ host }&name=${ name }`) let { data } = await $api.existSSH(host.value)
if (!host) { if (data) return window.open(`/terminal?host=${host.value}&name=${name.value}`)
if (!host.value) {
return ElMessage({ return ElMessage({
message: '请等待获取服务器ip或刷新页面重试', message: '请等待获取服务器ip或刷新页面重试',
type: 'warning', type: 'warning',
center: true center: true
}) })
} }
this.tempHost = host tempHost.value = host.value
this.sshFormVisible = true sshFormVisible.value = true
}, }
async handleRemoveSSH() {
ElMessageBox.confirm( const handleRemoveSSH = async () => {
'确认删除SSH凭证', ElMessageBox.confirm('确认删除SSH凭证', 'Warning', {
'Warning',
{
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
} }).then(async () => {
) let { data } = await $api.removeSSH(host.value)
.then(async () => {
let { host } = this
let { data } = await this.$api.removeSSH(host)
ElMessage({ ElMessage({
message: data, message: data,
type: 'success', type: 'success',
center: true center: true
}) })
}) })
}, }
handleRemoveHost() {
ElMessageBox.confirm( const handleRemoveHost = async () => {
'确认删除主机', ElMessageBox.confirm('确认删除主机', 'Warning', {
'Warning',
{
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
} }).then(async () => {
) let { data } = await $api.removeHost({ host: host.value })
.then(async () => {
let { host } = this
let { data } = await this.$api.removeHost({ host })
ElMessage({ ElMessage({
message: data, message: data,
type: 'success', type: 'success',
center: true center: true
}) })
this.$emit('update-list') 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;

View File

@ -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,21 +103,12 @@
</el-dialog> </el-dialog>
</template> </template>
<script> <script setup>
const resetForm = () => { import { ref, reactive, computed, watch, getCurrentInstance, nextTick } from 'vue'
return {
group: 'default', const { proxy: { $api, $message } } = getCurrentInstance()
name: '',
host: '', const props = defineProps({
expired: null,
expiredNotify: false,
consoleUrl: '',
remark: ''
}
}
export default {
name: 'HostForm',
props: {
show: { show: {
required: true, required: true,
type: Boolean type: Boolean
@ -127,14 +118,23 @@ export default {
type: Object, type: Object,
default: null default: null
} }
}, })
emits: ['update:show', 'update-list', 'closed',], const emit = defineEmits(['update:show', 'update-list', 'closed'])
data() {
return { const resetForm = () => ({
hostForm: resetForm(), group: 'default',
oldHost: '', name: '',
groupList: [], host: '',
rules: { expired: null,
expiredNotify: false,
consoleUrl: '',
remark: ''
})
const hostForm = reactive(resetForm())
const oldHost = ref('')
const groupList = ref([])
const rules = reactive({
group: { required: true, message: '选择一个分组' }, group: { required: true, message: '选择一个分组' },
name: { required: true, message: '输入主机别名', trigger: 'change' }, name: { required: true, message: '输入主机别名', trigger: 'change' },
host: { required: true, message: '输入IP/域名', trigger: 'change' }, host: { required: true, message: '输入IP/域名', trigger: 'change' },
@ -142,72 +142,61 @@ export default {
expiredNotify: { required: false }, expiredNotify: { required: false },
consoleUrl: { required: false }, consoleUrl: { required: false },
remark: { 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 formRef = ref(null)
handleSave() {
this.formRef.validate() const visible = computed({
.then(async () => { get: () => props.show,
if(!this.hostForm.expired || !this.hostForm.expiredNotify) { set: (newVal) => emit('update:show', newVal)
this.hostForm.expired = null })
this.hostForm.expiredNotify = false
} const title = computed(() => props.defaultData ? '修改服务器' : '新增服务器')
if(this.defaultData) {
let { oldHost } = this watch(() => props.show, (newVal) => {
let { msg } = await this.$api.updateHost(Object.assign({}, this.hostForm, { oldHost })) if (!newVal) return
this.$message({ type: 'success', center: true, message: msg }) getGroupList()
}else { })
let { msg } = await this.$api.saveHost(this.hostForm)
this.$message({ type: 'success', center: true, message: msg }) const getGroupList = () => {
} $api.getGroupList()
this.visible = false .then(({ data }) => {
this.$emit('update-list') groupList.value = data
this.hostForm = resetForm()
}) })
}
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>

View File

@ -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,72 +81,72 @@
</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)
const emailForm = reactive({
target: 'qq', target: 'qq',
auth: { auth: {
user: '', user: '',
pass: '' pass: ''
} }
}, })
rules: {
const rules = reactive({
'auth.user': { required: true, type: 'email', message: '需输入邮箱', trigger: 'change' }, 'auth.user': { required: true, type: 'email', message: '需输入邮箱', trigger: 'change' },
'auth.pass': { required: true, message: '需输入SMTP授权码', trigger: 'change' } 'auth.pass': { required: true, message: '需输入SMTP授权码', trigger: 'change' }
} })
}
}, const getUserEmailList = () => {
mounted() { loading.value = true
this.getUserEmailList() $api.getUserEmailList()
this.getSupportEmailList()
},
methods: {
getUserEmailList() {
this.loading = true
this.$api.getUserEmailList()
.then(({ data }) => { .then(({ data }) => {
this.userEmailList = data.map(item => { userEmailList.value = data.map(item => {
item.loading = false item.loading = false
return item return item
}) })
}) })
.finally(() => this.loading = false) .finally(() => loading.value = false)
}, }
getSupportEmailList() {
this.$api.getSupportEmailList() const getSupportEmailList = () => {
$api.getSupportEmailList()
.then(({ data }) => { .then(({ data }) => {
this.supportEmailList = data supportEmailList.value = data
}) })
}, }
addEmail() {
let emailFormRef = this.$refs['email-form'] const addEmail = () => {
emailFormRef.validate() emailFormRef.value.validate()
.then(() => { .then(() => {
this.$api.updateUserEmailList({ ...this.emailForm }) $api.updateUserEmailList({ ...emailForm })
.then(() => { .then(() => {
this.$message.success('添加成功, 点击[测试]按钮发送测试邮件') $message.success('添加成功, 点击[测试]按钮发送测试邮件')
let { target } = this.emailForm let { target } = emailForm
this.emailForm = { target, auth: { user: '', pass: '' } } emailForm.target = target
this.$nextTick(() => emailFormRef.resetFields()) emailForm.auth.user = ''
this.getUserEmailList() emailForm.auth.pass = ''
getUserEmailList()
}) })
}) })
}, }
pushTestEmail(row) {
const pushTestEmail = (row) => {
row.loading = true row.loading = true
const { email: toEmail } = row const { email: toEmail } = row
this.$api.pushTestEmail({ isTest: true, toEmail }) $api.pushTestEmail({ isTest: true, toEmail })
.then(() => { .then(() => {
this.$message.success(`发送成功, 请检查邮箱: ${ toEmail }`) $message.success(`发送成功, 请检查邮箱: ${toEmail}`)
}) })
.catch((error) => { .catch((error) => {
this.$notification({ $notification({
title: '发送测试邮件失败, 请检查邮箱SMTP配置', title: '发送测试邮件失败, 请检查邮箱SMTP配置',
message: error.response?.data.msg, message: error.response?.data.msg,
type: 'error' type: 'error'
@ -155,10 +155,11 @@ export default {
.finally(() => { .finally(() => {
row.loading = false row.loading = false
}) })
}, }
deleteUserEmail({ email }) {
this.$messageBox.confirm( const deleteUserEmail = ({ email }) => {
`确认删除邮箱:${ email }`, $messageBox.confirm(
`确认删除邮箱:${email}`,
'Warning', 'Warning',
{ {
confirmButtonText: '确定', confirmButtonText: '确定',
@ -167,13 +168,16 @@ export default {
} }
) )
.then(async () => { .then(async () => {
await this.$api.deleteUserEmail(email) await $api.deleteUserEmail(email)
this.$message.success('success') $message.success('success')
this.getUserEmailList() getUserEmailList()
}) })
}
}
} }
onMounted(() => {
getUserEmailList()
getSupportEmailList()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -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,41 +118,43 @@
</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)
computed: { const updateFormRef = ref(null)
hostGroupInfo() {
let total = this.$store.hostList.length const hostGroupInfo = computed(() => {
let notGroupCount = this.$store.hostList.reduce((prev, next) => { const total = $store.hostList.length
if(!next.group) prev++ const notGroupCount = $store.hostList.reduce((prev, next) => {
if (!next.group) prev++
return prev return prev
}, 0) }, 0)
return { total, notGroupCount } return { total, notGroupCount }
}, })
list() {
return this.groupList.map(item => { const list = computed(() => {
let hosts = this.$store.hostList.reduce((prev, next) => { return groupList.value.map(item => {
if(next.group === item.id) { const hosts = $store.hostList.reduce((prev, next) => {
if (next.group === item.id) {
prev.count++ prev.count++
prev.list.push(next) prev.list.push(next)
} }
@ -161,67 +162,69 @@ export default {
}, { count: 0, list: [] }) }, { count: 0, list: [] })
return { ...item, hosts } return { ...item, hosts }
}) })
} })
},
mounted() { const getGroupList = () => {
this.getGroupList() loading.value = true
}, $api.getGroupList()
methods: {
getGroupList() {
this.loading = true
this.$api.getGroupList()
.then(({ data }) => { .then(({ data }) => {
this.groupList = data groupList.value = data
this.groupForm.index = data.length groupForm.index = data.length
}) })
.finally(() => this.loading = false) .finally(() => loading.value = false)
}, }
addGroup() {
let formRef = this.$refs['group-form'] const addGroup = () => {
formRef.validate() groupFormRef.value.validate()
.then(() => { .then(() => {
const { name, index } = this.groupForm const { name, index } = groupForm
this.$api.addGroup({ name, index }) $api.addGroup({ name, index })
.then(() => { .then(() => {
this.$message.success('success') $message.success('success')
this.groupForm = { name: '', index: '' } groupForm.name = ''
this.$nextTick(() => formRef.resetFields()) groupForm.index = ''
this.getGroupList() getGroupList()
}) })
}) })
}, }
handleChange({ id, name, index }) {
this.updateForm = { id, name, index } const handleChange = ({ id, name, index }) => {
this.visible = true updateForm.id = id
}, updateForm.name = name
updateGroup() { updateForm.index = index
let formRef = this.$refs['update-form'] visible.value = true
formRef.validate() }
const updateGroup = () => {
updateFormRef.value.validate()
.then(() => { .then(() => {
const { id, name, index } = this.updateForm const { id, name, index } = updateForm
this.$api.updateGroup(id, { name, index }) $api.updateGroup(id, { name, index })
.then(() => { .then(() => {
this.$message.success('success') $message.success('success')
this.visible = false visible.value = false
this.getGroupList() getGroupList()
}) })
}) })
}, }
deleteGroup({ id, name }) {
this.$messageBox.confirm( `确认删除分组:${ name }`, 'Warning', { const deleteGroup = ({ id, name }) => {
$messageBox.confirm(`确认删除分组:${name}`, 'Warning', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}) })
.then(async () => { .then(async () => {
await this.$api.deleteGroup(id) await $api.deleteGroup(id)
await this.$store.getHostList() await $store.getHostList()
this.$message.success('success') $message.success('success')
this.getGroupList() getGroupList()
}) })
}
}
} }
onMounted(() => {
getGroupList()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -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: {
getNotifyList(flag = true) {
if(flag) this.notifyListLoading = true
this.$api.getNotifyList()
.then(({ data }) => { .then(({ data }) => {
this.notifyList = data.map((item) => { notifyList.value = data.map((item) => {
item.loading = false item.loading = false
return item return item
}) })
}) })
.finally(() => this.notifyListLoading = false) .finally(() => notifyListLoading.value = false)
}, }
async handleChangeSw(row) {
const handleChangeSw = async (row) => {
row.loading = true row.loading = true
const { type, sw } = row const { type, sw } = row
try { try {
await this.$api.updateNotifyList({ type, sw }) await $api.updateNotifyList({ type, sw })
// if(this.userEmailList.length === 0) this.$message.warning(', ') // if (this.userEmailList.length === 0) $message.warning(', ')
} finally { } finally {
row.loading = true row.loading = false
}
this.getNotifyList(false)
}
} }
getNotifyList(false)
} }
onMounted(() => {
getNotifyList()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -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: ''
}, })
rules: { const rules = reactive({
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 = () => {
computed: { formRef.value.validate()
formRef() {
return this.$refs['form']
}
},
methods: {
handleUpdate() {
this.formRef.validate()
.then(async () => { .then(async () => {
let { oldPwd, newPwd, confirmPwd } = this.formData let { oldPwd, newPwd, confirmPwd } = formData
if(newPwd !== confirmPwd) return this.$message.error({ center: true, message: '两次密码输入不一致' }) if(newPwd !== confirmPwd) return $message.error({ center: true, message: '两次密码输入不一致' })
oldPwd = RSAEncrypt(oldPwd) oldPwd = RSAEncrypt(oldPwd)
newPwd = RSAEncrypt(newPwd) newPwd = RSAEncrypt(newPwd)
let { msg } = await this.$api.updatePwd({ oldPwd, newPwd }) let { msg } = await $api.updatePwd({ oldPwd, newPwd })
this.$message({ type: 'success', center: true, message: msg }) $message({ type: 'success', center: true, message: msg })
this.formData = { oldPwd: '', newPwd: '', confirmPwd: '' } formData.oldPwd = ''
this.formRef.resetFields() formData.newPwd = ''
formData.confirmPwd = ''
formRef.value.resetFields()
}) })
}
}
} }
</script> </script>

View File

@ -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()
},
methods: {
handleLookupLoginRecord() {
this.loading = true
this.$api.getLoginRecord()
.then(({ data }) => { .then(({ data }) => {
this.loginRecordList = data.map((item) => { loginRecordList.value = data.map((item) => {
item.date = this.$tools.formatTimestamp(item.date) item.date = $tools.formatTimestamp(item.date)
return item return item
}) })
}) })
.finally(() => { .finally(() => {
this.loading = false loading.value = false
}) })
}
}
} }
onMounted(() => {
handleLookupLoginRecord()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
</style> </style>

View File

@ -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) {
// console.log('', index)
this.targetIndex = index
},
dragenter(e, curIndex) {
e.preventDefault() e.preventDefault()
if (this.targetIndex !== curIndex) { if (targetIndex.value !== curIndex) {
// console.log('', curIndex) let target = list.value.splice(targetIndex.value, 1)[0]
let target = this.list.splice(this.targetIndex, 1)[0] list.value.splice(curIndex, 0, target)
this.list.splice(curIndex, 0, target) targetIndex.value = curIndex
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 {

View File

@ -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',
components: {
NotifyList,
EmailList,
Sort,
Record,
Group,
Password
},
props: {
show: { show: {
required: true, required: true,
type: Boolean type: Boolean
} }
}, })
emits: ['update:show', 'update-list',],
data() { const emit = defineEmits(['update:show', 'update-list'])
return {
} const visible = computed({
}, get: () => props.show,
computed: { set: (newVal) => emit('update:show', newVal)
visible: { })
get() {
return this.show const emitUpdateList = () => {
}, emit('update-list')
set(newVal) {
this.$emit('update:show', newVal)
}
}
}
} }
</script> </script>

View File

@ -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,19 +86,17 @@
<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',
props: {
show: { show: {
required: true, required: true,
type: Boolean type: Boolean
@ -111,11 +109,13 @@ export default {
required: true, required: true,
type: String type: String
} }
}, })
emits: ['update:show',],
data() { const emit = defineEmits(['update:show'])
return {
sshForm: { const formRef = ref(null)
const privateKeyRef = ref(null)
const sshForm = reactive({
host: '', host: '',
port: 22, port: 22,
username: '', username: '',
@ -123,12 +123,14 @@ export default {
password: '', password: '',
privateKey: '', privateKey: '',
command: '' command: ''
}, })
defaultUsers: [
const defaultUsers = [
{ value: 'root' }, { value: 'root' },
{ value: 'ubuntu' }, { value: 'ubuntu' },
], ]
rules: {
const rules = reactive({
host: { required: true, message: '需输入主机', trigger: 'change' }, host: { required: true, message: '需输入主机', trigger: 'change' },
port: { required: true, message: '需输入端口', trigger: 'change' }, port: { required: true, message: '需输入端口', trigger: 'change' },
username: { required: true, message: '需输入用户名', trigger: 'change' }, username: { required: true, message: '需输入用户名', trigger: 'change' },
@ -136,71 +138,65 @@ export default {
password: { required: true, message: '需输入密码', trigger: 'change' }, password: { required: true, message: '需输入密码', trigger: 'change' },
privateKey: { required: true, message: '需输入密钥', trigger: 'change' }, privateKey: { required: true, message: '需输入密钥', trigger: 'change' },
command: { required: false } command: { required: false }
} })
}
}, const { proxy: { $api, $tools } } = getCurrentInstance()
computed: {
visible: { const visible = computed({
get() { get() {
return this.show return props.show
}, },
set(newVal) { set(newVal) {
this.$emit('update:show', newVal) emit('update:show', newVal)
} }
}, })
formRef() {
return this.$refs['form'] watch(() => props.tempHost, (newVal) => {
} sshForm.host = newVal
}, })
watch: {
tempHost: { const handleClickUploadBtn = () => {
handler(newVal) { privateKeyRef.value.click()
this.sshForm.host = newVal }
}
} const handleSelectPrivateKeyFile = (event) => {
},
methods: {
handleClickUploadBtn() {
this.$refs['privateKey'].click()
},
handleSelectPrivateKeyFile(event) {
let file = event.target.files[0] let file = event.target.files[0]
let reader = new FileReader() let reader = new FileReader()
reader.onload = (e) => { reader.onload = (e) => {
this.sshForm.privateKey = e.target.result sshForm.privateKey = e.target.result
this.$refs['privateKey'].value = '' privateKeyRef.value.value = ''
} }
reader.readAsText(file) reader.readAsText(file)
}, }
handleSaveSSH() {
this.formRef.validate() const handleSaveSSH = () => {
formRef.value.validate()
.then(async () => { .then(async () => {
let randomKey = randomStr(16) let randomKey = randomStr(16)
let formData = JSON.parse(JSON.stringify(this.sshForm)) let formData = JSON.parse(JSON.stringify(sshForm))
// //
if(formData.password) formData.password = AESEncrypt(formData.password, randomKey) if (formData.password) formData.password = AESEncrypt(formData.password, randomKey)
if(formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, randomKey) if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, randomKey)
formData.randomKey = RSAEncrypt(randomKey) formData.randomKey = RSAEncrypt(randomKey)
await $api.updateSSH(formData) await $api.updateSSH(formData)
this.$notification({ ElNotification({
title: '保存成功', title: '保存成功',
message: '下次点击 [Web SSH] 可直接登录终端\n如无法登录请 [移除凭证] 后重新添加', message: '下次点击 [Web SSH] 可直接登录终端\n如无法登录请 [移除凭证] 后重新添加',
type: 'success' type: 'success'
}) })
this.visible = false visible.value = false
// this.$message({ type: 'success', center: true, message: data })
// setTimeout(() => {
// window.open(`/terminal?host=${ this.tempHost }&name=${ this.name }`)
// }, 1000)
}) })
}, }
userSearch(keyword, cb) {
const userSearch = (keyword, cb) => {
let res = keyword let res = keyword
? this.defaultUsers.filter((item) => item.value.includes(keyword)) ? defaultUsers.filter((item) => item.value.includes(keyword))
: this.defaultUsers : defaultUsers
cb(res) cb(res)
} }
}
const clearFormInfo = () => {
nextTick(() => formRef.value.resetFields())
} }
</script> </script>

View File

@ -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 () => {
},
mounted() {
this.getHostList()
},
beforeUnmount() {
this.socket?.close && this.socket.close()
},
methods: {
handleLogout() {
this.$store.clearJwtToken()
this.$message({ type: 'success', message: '已安全退出', center: true })
this.$router.push('/login')
},
async getHostList() {
try { try {
this.loading = true loading.value = true
await this.$store.getHostList() await $store.getHostList()
this.connectIo() connectIo()
} catch(err) { } catch (err) {
this.loading = false loading.value = false
} }
}, }
connectIo() {
let socket = io(this.$serviceURI, { const connectIo = () => {
let socketInstance = io($serviceURI, {
path: '/clients', path: '/clients',
forceNew: true, // forceNew: true,
reconnectionDelay: 5000, reconnectionDelay: 5000,
reconnectionAttempts: 2 // 5s reconnectionAttempts: 2
}) })
this.socket = socket socket.value = socketInstance
socket.on('connect', () => { socketInstance.on('connect', () => {
let flag = 5 let flag = 5
this.loading = false loading.value = false
console.log('clients websocket 已连接: ', socket.id) console.log('clients websocket 已连接: ', socketInstance.id)
let token = this.$store.token let token = $store.token
socket.emit('init_clients_data', { token }) socketInstance.emit('init_clients_data', { token })
socket.on('clients_data', (data) => { socketInstance.on('clients_data', (data) => {
if((flag++ % 5) === 0) this.$store.getHostPing() if ((flag++ % 5) === 0) $store.getHostPing()
this.hostListStatus = this.$store.hostList.map(item => { hostListStatus.value = $store.hostList.map(item => {
const { host } = item const { host } = item
if(data[host] === null) return { ...item }// null if (data[host] === null) return { ...item }
return Object.assign({}, item, data[host]) return Object.assign({}, item, data[host])
}) })
}) })
socket.on('token_verify_fail', (message) => { socketInstance.on('token_verify_fail', (message) => {
this.$notification({ $notification({
title: '鉴权失败', title: '鉴权失败',
message, message,
type: 'error' type: 'error'
}) })
this.$router.push('/login') $router.push('/login')
}) })
}) })
socket.on('disconnect', () => { socketInstance.on('disconnect', () => {
// this.$notification({
// title: 'server websocket error',
// message: '',
// type: 'error'
// })
console.error('clients websocket 连接断开') console.error('clients websocket 连接断开')
}) })
socket.on('connect_error', (message) => { socketInstance.on('connect_error', (message) => {
this.loading = false loading.value = false
console.error('clients websocket 连接出错: ', message) 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 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;

View File

@ -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)
}, if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
async created() { loading.value = true
if(localStorage.getItem('jwtExpires')) this.loginForm.jwtExpires = Number(localStorage.getItem('jwtExpires')) $api.login({ ciphertext, jwtExpires })
// console.log(localStorage.getItem('jwtExpires'))
//
let { data } = await this.$api.getPubPem()
if (!data) return (this.notKey = true)
localStorage.setItem('publicKey', data)
},
methods: {
handleLogin() {
this.$refs['login-form'].validate().then(() => {
let { isSession, loginForm: { pwd, jwtExpires } } = this
if(isSession) jwtExpires = '12h' // token12hsessionStroage
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 }) => { .then(({ data, msg }) => {
let { token } = data const { token } = data
this.$store.setJwtToken(token, isSession) $store.setJwtToken(token, isSession.value)
this.$message.success({ message: msg || 'success', center: true }) $message.success({ message: msg || 'success', center: true })
this.$router.push('/') $router.push('/')
}) })
.finally(() => { .finally(() => {
this.loading = false 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;

View File

@ -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,29 +160,23 @@
</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: { const props = defineProps({
token: { token: {
required: true, required: true,
type: String type: String
@ -222,147 +188,139 @@ export default {
visible: { visible: {
required: true, required: true,
type: Boolean type: Boolean
}
}, },
emits: ['connect-sftp', 'click-input-command',], showInputCommand: {
data() { required: true,
return { type: Boolean
socket: null,
name: '',
hostData: null,
ping: 0,
pingTimer: null,
sftpStatus: false,
inputCommandStatus: false
}
}, },
computed: { })
ipInfo() {
return this.hostData?.ipInfo || {} const emit = defineEmits(['update:inputCommandStyle', 'connect-sftp', 'click-input-command'])
},
isError() { const socket = ref(null)
return !Boolean(this.hostData?.osInfo) // const name = ref('')
}, const hostData = ref(null)
cpuInfo() { const ping = ref(0)
return this.hostData?.cpuInfo || {} const pingTimer = ref(null)
}, const sftpStatus = ref(false)
memInfo() {
return this.hostData?.memInfo || {} const ipInfo = computed(() => hostData.value?.ipInfo || {})
}, const isError = computed(() => !Boolean(hostData.value?.osInfo))
osInfo() { const cpuInfo = computed(() => hostData.value?.cpuInfo || {})
return this.hostData?.osInfo || {} const memInfo = computed(() => hostData.value?.memInfo || {})
}, const osInfo = computed(() => hostData.value?.osInfo || {})
driveInfo() { const driveInfo = computed(() => hostData.value?.driveInfo || {})
return this.hostData?.driveInfo || {} const netstatInfo = computed(() => {
}, let { total: netTotal, ...netCards } = hostData.value?.netstatInfo || {}
netstatInfo() {
let { total: netTotal, ...netCards } = this.hostData?.netstatInfo || {}
return { netTotal, netCards: netCards || {} } return { netTotal, netCards: netCards || {} }
}, })
openedCount() { const openedCount = computed(() => hostData.value?.openedCount || 0)
return this.hostData?.openedCount || 0 const cpuUsage = computed(() => Number(cpuInfo.value?.cpuUsage) || 0)
}, const usedMemPercentage = computed(() => Number(memInfo.value?.usedMemPercentage) || 0)
cpuUsage() { const usedPercentage = computed(() => Number(driveInfo.value?.usedPercentage) || 0)
return Number(this.cpuInfo?.cpuUsage) || 0 const output = computed(() => {
}, let outputMb = Number(netstatInfo.value.netTotal?.outputMb) || 0
usedMemPercentage() { if (outputMb >= 1) return `${outputMb.toFixed(2)} MB/s`
return Number(this.memInfo?.usedMemPercentage) || 0 return `${(outputMb * 1024).toFixed(1)} KB/s`
}, })
usedPercentage() { const input = computed(() => {
return Number(this.driveInfo?.usedPercentage) || 0 let inputMb = Number(netstatInfo.value.netTotal?.inputMb) || 0
}, if (inputMb >= 1) return `${inputMb.toFixed(2)} MB/s`
output() { return `${(inputMb * 1024).toFixed(1)} KB/s`
let outputMb = Number(this.netstatInfo.netTotal?.outputMb) || 0 })
if(outputMb >= 1 ) return `${ outputMb.toFixed(2) } MB/s` const inputCommandStyle = computed({
return `${ (outputMb * 1024).toFixed(1) } KB/s` get: () => props.showInputCommand,
}, set: (val) => {
input() { emit('update:inputCommandStyle', val)
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 || '' const handleSftp = () => {
if(!this.host || !this.name) return this.$message.error('参数错误') sftpStatus.value = !sftpStatus.value
this.connectIO() emit('connect-sftp', sftpStatus.value)
}, }
beforeUnmount() {
this.socket && this.socket.close() const clickInputCommand = () => {
this.pingTimer && clearInterval(this.pingTimer) inputCommandStyle.value = true
}, emit('click-input-command')
methods: { }
handleSftp() {
this.sftpStatus = !this.sftpStatus const connectIO = () => {
this.$emit('connect-sftp', this.sftpStatus) const { host, token } = props
}, socket.value = socketIo($serviceURI, {
clickInputCommand() {
this.inputCommandStatus = true
this.$emit('click-input-command')
},
connectIO() {
let { host, token } = this
this.socket = socketIo(this.$serviceURI, {
path: '/host-status', path: '/host-status',
forceNew: true, // forceNew: true,
timeout: 5000, timeout: 5000,
reconnectionDelay: 3000, reconnectionDelay: 3000,
reconnectionAttempts: 3 reconnectionAttempts: 3
}) })
this.socket.on('connect', () => {
console.log('/host-status socket已连接', this.socket.id) socket.value.on('connect', () => {
this.socket.emit('init_host_data', { token, host }) console.log('/host-status socket已连接', socket.value.id)
this.getHostPing() socket.value.emit('init_host_data', { token, host })
this.socket.on('host_data', (hostData) => { getHostPing()
if(!hostData) return this.hostData = null socket.value.on('host_data', (data) => {
this.hostData = hostData if (!data) return hostData.value = null
hostData.value = data
}) })
}) })
this.socket.on('connect_error', (err) => { socket.value.on('connect_error', (err) => {
console.error('host status websocket 连接错误:', err) console.error('host status websocket 连接错误:', err)
this.$notification({ $notification({
title: '连接客户端失败(重连中...)', title: '连接客户端失败(重连中...)',
message: '请检查客户端服务是否正常', message: '请检查客户端服务是否正常',
type: 'error' type: 'error'
}) })
}) })
this.socket.on('disconnect', () => { socket.value.on('disconnect', () => {
this.hostData = null hostData.value = null
this.$notification({ $notification({
title: '客户端连接主动断开(重连中...)', title: '客户端连接主动断开(重连中...)',
message: '请检查客户端服务是否正常', message: '请检查客户端服务是否正常',
type: 'error' type: 'error'
}) })
}) })
}, }
async handleCopy() {
await navigator.clipboard.writeText(this.host) const handleCopy = async () => {
this.$message.success({ message: 'success', center: true }) await navigator.clipboard.writeText(props.host)
}, $message.success({ message: 'success', center: true })
handleColor(num) { }
if(num < 65) return '#8AE234'
if(num < 85) return '#FFD700' const handleColor = (num) => {
if(num < 90) return '#FFFF33' if (num < 65) return '#8AE234'
if(num <= 100) return '#FF3333' if (num < 85) return '#FFD700'
}, if (num < 90) return '#FFFF33'
getHostPing() { if (num <= 100) return '#FF3333'
this.pingTimer = setInterval(() => { }
this.$tools.ping(`http://${ this.host }:22022`)
const getHostPing = () => {
pingTimer.value = setInterval(() => {
$tools.ping(`http://${props.host}:22022`)
.then(res => { .then(res => {
this.ping = res ping.value = res
if(!import.meta.env.DEV) { if (!import.meta.env.DEV) {
// console.clear()
console.warn('Please tick \'Preserve Log\'') console.warn('Please tick \'Preserve Log\'')
} }
}) })
}, 3000) }, 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;
} }

View File

@ -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,10 +143,8 @@ 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', const props = defineProps({
components: { CodeEdit },
props: {
token: { token: {
required: true, required: true,
type: String type: String
@ -154,16 +153,18 @@ export default {
required: true, required: true,
type: String type: String
} }
}, })
emits: ['resize',],
data() { const emit = defineEmits(['resize'])
return {
visible: false, const { proxy: { $notification, $message, $messageBox, $serviceURI, $nextTick } } = getCurrentInstance()
originalCode: '',
filename: '', const visible = ref(false)
filterKey: '', const originalCode = ref('')
socket: null, const filename = ref('')
icons: { const filterKey = ref('')
const socket = ref(null)
const icons = {
'-': fileIcon, '-': fileIcon,
l: linkIcon, l: linkIcon,
d: dirIcon, d: dirIcon,
@ -171,379 +172,362 @@ export default {
p: unknowIcon, p: unknowIcon,
s: unknowIcon, s: unknowIcon,
b: unknowIcon b: unknowIcon
}, }
paths: ['/',], const paths = ref(['/',])
rootLs: [], const rootLs = ref([])
childDir: [], const childDir = ref([])
childDirLoading: false, const childDirLoading = ref(false)
curTarget: null, const curTarget = ref(null)
showFileProgress: false, const showFileProgress = ref(false)
upFileProgress: 0, const upFileProgress = ref(0)
curUploadFileName: '' const curUploadFileName = ref('')
} const adjustRef = ref(null)
}, const childDirRef = ref(null)
computed: { const uploadFileRef = ref(null)
curPath() {
return this.paths.join('/').replace(/\/{2,}/g, '/') const curPath = computed(() => paths.value.join('/').replace(/\/{2,}/g, '/'))
}, const fileList = computed(() => childDir.value.filter(({ name }) => name.includes(filterKey.value)))
fileList() {
return this.childDir.filter(({ name }) => name.includes(this.filterKey)) onMounted(() => {
} connectSftp()
}, adjustHeight()
mounted() { })
this.connectSftp()
this.adjustHeight() onBeforeUnmount(() => {
}, if (socket.value) socket.value.close()
beforeUnmount() { })
this.socket && this.socket.close()
}, const connectSftp = () => {
methods: { socket.value = io($serviceURI, {
connectSftp() {
let { host, token } = this
this.socket = io(this.$serviceURI, {
path: '/sftp', path: '/sftp',
forceNew: false, // forceNew: false,
reconnectionAttempts: 1 // reconnectionAttempts: 1
}) })
this.socket.on('connect', () => { socket.value.on('connect', () => {
console.log('/sftp socket已连接', this.socket.id) console.log('/sftp socket已连接', socket.value.id)
this.listenSftp() listenSftp()
// socket.value.emit('create', { host: props.host, token: props.token })
this.socket.emit('create', { host, token }) socket.value.on('root_ls', (tree) => {
this.socket.on('root_ls', (tree) => { let temp = sortDirTree(tree).filter((item) => isDir(item.type))
// console.log(tree)
let temp = sortDirTree(tree).filter((item) => isDir(item.type)) //
temp.unshift({ name: '/', type: 'd' }) temp.unshift({ name: '/', type: 'd' })
this.rootLs = temp rootLs.value = temp
}) })
this.socket.on('create_fail', (message) => { socket.value.on('create_fail', (message) => {
// console.error(message) $notification({
this.$notification({
title: 'Sftp连接失败', title: 'Sftp连接失败',
message, message,
type: 'error' type: 'error'
}) })
}) })
this.socket.on('token_verify_fail', () => { socket.value.on('token_verify_fail', () => {
this.$notification({ $notification({
title: 'Error', title: 'Error',
message: 'token校验失败需重新登录', message: 'token校验失败需重新登录',
type: 'error' type: 'error'
}) })
// this.$router.push('/login')
}) })
}) })
this.socket.on('disconnect', () => { socket.value.on('disconnect', () => {
console.warn('sftp websocket 连接断开') console.warn('sftp websocket 连接断开')
if(this.showFileProgress) { if (showFileProgress.value) {
this.$notification({ $notification({
title: '上传失败', title: '上传失败',
message: '请检查socket服务是否正常', message: '请检查socket服务是否正常',
type: 'error' type: 'error'
}) })
this.handleRefresh() handleRefresh()
this.resetFileStatusFlag() resetFileStatusFlag()
} }
}) })
this.socket.on('connect_error', (err) => { socket.value.on('connect_error', (err) => {
console.error('sftp websocket 连接错误:', err) console.error('sftp websocket 连接错误:', err)
this.$notification({ $notification({
title: 'sftp连接失败', title: 'sftp连接失败',
message: '请检查socket服务是否正常', message: '请检查socket服务是否正常',
type: 'error' type: 'error'
}) })
}) })
}, }
// socketon
listenSftp() { const listenSftp = () => {
this.socket.on('dir_ls', (dirLs) => { socket.value.on('dir_ls', (dirLs) => {
// console.log('dir_ls: ', dirLs) childDir.value = sortDirTree(dirLs)
this.childDir = sortDirTree(dirLs) childDirLoading.value = false
this.childDirLoading = false
}) })
this.socket.on('not_exists_dir', (errMsg) => { socket.value.on('not_exists_dir', (errMsg) => {
this.$message.error(errMsg) $message.error(errMsg)
this.childDirLoading = false childDirLoading.value = false
}) })
this.socket.on('rm_success', (res) => { socket.value.on('rm_success', (res) => {
this.$message.success(res) $message.success(res)
this.childDirLoading = false childDirLoading.value = false
this.handleRefresh() handleRefresh()
}) })
// this.socket.on('down_dir_success', (res) => { socket.value.on('down_file_success', (res) => {
// console.log(res)
// this.$message.success(res)
// this.childDirLoading = false
// })
this.socket.on('down_file_success', (res) => {
const { buffer, name } = res const { buffer, name } = res
downloadFile({ buffer, name }) downloadFile({ buffer, name })
this.$message.success('success') $message.success('success')
this.resetFileStatusFlag() resetFileStatusFlag()
}) })
this.socket.on('preview_file_success', (res) => { socket.value.on('preview_file_success', (res) => {
const { buffer, name } = res const { buffer, name } = res
console.log('preview_file: ', name, buffer) originalCode.value = new TextDecoder().decode(buffer)
// String.fromCharCode.apply(null, new Uint8Array(temp1)) filename.value = name
this.originalCode = new TextDecoder().decode(buffer) visible.value = true
this.filename = name
this.visible = true
}) })
this.socket.on('sftp_error', (res) => { socket.value.on('sftp_error', (res) => {
console.log('操作失败:', res) $message.error(res)
this.$message.error(res) resetFileStatusFlag()
this.resetFileStatusFlag()
}) })
this.socket.on('up_file_progress', (res) => { socket.value.on('up_file_progress', (res) => {
// console.log(':', res)
// 50%50%
let progress = Math.ceil(50 + (res / 2)) let progress = Math.ceil(50 + (res / 2))
this.upFileProgress = progress > 100 ? 100 : progress upFileProgress.value = progress > 100 ? 100 : progress
}) })
this.socket.on('down_file_progress', (res) => { socket.value.on('down_file_progress', (res) => {
// console.log(':', res) upFileProgress.value = res
this.upFileProgress = res
}) })
}, }
openRootChild(item) {
const openRootChild = (item) => {
const { name, type } = item const { name, type } = item
if(isDir(type)) { if (isDir(type)) {
this.childDirLoading = true childDirLoading.value = true
this.paths.length = 2 paths.value.length = 2
this.paths[1] = name paths.value[1] = name
this.$refs['child-dir']?.scrollTo(0, 0) $nextTick(() => {
this.openDir() if (childDirRef.value) childDirRef.value.scrollTo(0, 0)
this.filterKey = '' // })
}else { openDir()
console.log('暂不支持打开文件', name, type) filterKey.value = ''
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 { } else {
this.$message.warning(`暂不支持打开文件${ name } ${ type }`) $message.warning(`暂不支持打开文件${name} ${type}`)
} }
}, }
handleSaveCode(code) {
// console.log('code: ', code) 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 file = new TextEncoder('utf-8').encode(code)
let name = this.filename let name = filename.value
const fullPath = this.getPath(name) const fullPath = getPath(name)
const targetPath = this.curPath const targetPath = curPath.value
this.socket.emit('up_file', { targetPath, fullPath, name, file }) socket.value.emit('up_file', { targetPath, fullPath, name, file })
}, }
handleClosedCode() {
this.filename = '' const handleClosedCode = () => {
this.originalCode = '' filename.value = ''
}, originalCode.value = ''
selectFile(item) { }
this.curTarget = item
}, const selectFile = (item) => {
handleReturn() { curTarget.value = item
if(this.paths.length === 1) return }
this.paths.pop()
this.openDir() const handleReturn = () => {
}, if (paths.value.length === 1) return
handleRefresh() { paths.value.pop()
this.openDir() openDir()
}, }
handleDownload() {
if(this.curTarget === null) return this.$message.warning('先选择一个文件') const handleRefresh = () => {
const { name, size, type } = this.curTarget openDir()
if(isDir(type)) return this.$message.error('暂不支持下载文件夹') }
this.$messageBox.confirm( `确认下载:${ name }`, 'Warning', {
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: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}) }).then(() => {
.then(() => { childDirLoading.value = true
this.childDirLoading = true const path = getPath(name)
const path = this.getPath(name) if (isDir(type)) {
if(isDir(type)) {
// '' // ''
// this.socket.emit('down_dir', path) } else if (isFile(type)) {
}else if(isFile(type)) { showFileProgress.value = true
this.showFileProgress = true socket.value.emit('down_file', { path, name, size, target: 'down' })
this.socket.emit('down_file', { path, name, size, target: 'down' }) } else {
}else { $message.error('不支持下载的文件类型')
this.$message.error('不支持下载的文件类型')
} }
}) })
}, }
handleDelete() {
if(this.curTarget === null) return this.$message.warning('先选择一个文件(夹)') const handleDelete = () => {
const { name, type } = this.curTarget if (curTarget.value === null) return $message.warning('先选择一个文件(夹)')
this.$messageBox.confirm( `确认删除:${ name }`, 'Warning', { const { name, type } = curTarget.value
$messageBox.confirm(`确认删除:${name}`, 'Warning', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}) }).then(() => {
.then(() => { childDirLoading.value = true
this.childDirLoading = true const path = getPath(name)
const path = this.getPath(name) if (isDir(type)) {
if(isDir(type)) { socket.value.emit('rm_dir', path)
this.socket.emit('rm_dir', path) } else {
}else { socket.value.emit('rm_file', path)
this.socket.emit('rm_file', path)
} }
}) })
}, }
async handleUpload(event) {
if(this.showFileProgress) return this.$message.warning('需等待当前任务完成') const handleUpload = async (event) => {
if (showFileProgress.value) return $message.warning('需等待当前任务完成')
let { files } = event.target let { files } = event.target
for(let file of files) { for (let file of files) {
console.log(file)
try { try {
await this.uploadFile(file) await uploadFile(file)
} catch (error) { } catch (error) {
this.$message.error(error) $message.error(error)
} }
} }
this.$refs['upload_file'].value = '' uploadFileRef.value = null
}, }
uploadFile(file) {
const uploadFile = (file) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(!file) return reject('file is not defined') if (!file) return reject('file is not defined')
if((file.size/1024/1024)> 1000) { if ((file.size / 1024 / 1024) > 1000) {
this.$message.warn('用网页传这么大文件你是认真的吗?') $message.warn('用网页传这么大文件你是认真的吗?')
} }
let reader = new FileReader() let reader = new
FileReader()
reader.onload = async (e) => { reader.onload = async (e) => {
// console.log('buffer:', e.target.result)
const { name } = file const { name } = file
const fullPath = this.getPath(name) const fullPath = getPath(name)
const targetPath = this.curPath const targetPath = curPath.value
this.curUploadFileName = name curUploadFileName.value = name
this.socket.emit('create_cache_dir', { targetPath, name }) socket.value.emit('create_cache_dir', { targetPath, name })
// socket.value.once('create_cache_success', async () => {
this.socket.once('create_cache_success', async () => {
let start = 0 let start = 0
let end = 0 let end = 0
let range = 1024 * 512 // 512KB const range = 1024 * 512 // 512KB
let size = file.size const size = file.size
let fileIndex = 0 let fileIndex = 0
let multipleFlag = false // once let multipleFlag = false
try { try {
console.log('=========开始上传分片=========') upFileProgress.value = 0
this.upFileProgress = 0 showFileProgress.value = true
this.showFileProgress = true childDirLoading.value = true
this.childDirLoading = true const totalSliceCount = Math.ceil(size / range)
let totalSliceCount = Math.ceil(size / range) while (end < size) {
while(end < size) {
fileIndex++ fileIndex++
end += range end += range
let sliceFile = file.slice(start, end) const sliceFile = file.slice(start, end)
start = end start = end
await this.uploadSliceFile({ name, sliceFile, fileIndex }) await uploadSliceFile({ name, sliceFile, fileIndex })
// 50%50% upFileProgress.value = parseInt((fileIndex / totalSliceCount * 100) / 2)
this.upFileProgress = parseInt((fileIndex / totalSliceCount * 100) / 2)
} }
console.log('=========分片上传完成(等待服务端上传至客户端)=========') socket.value.emit('up_file_slice_over', { name, fullPath, range, size })
this.socket.emit('up_file_slice_over', { name, fullPath, range, size }) socket.value.once('up_file_success', (res) => {
this.socket.once('up_file_success', (res) => { if (multipleFlag) return
if(multipleFlag) return handleRefresh()
console.log('=========服务端上传至客户端上传完成✔=========') resetFileStatusFlag()
// console.log('up_file_success:', res)
// this.$message.success(res)
this.handleRefresh()
this.resetFileStatusFlag()
multipleFlag = true multipleFlag = true
resolve() resolve()
}) })
this.socket.once('up_file_fail', (res) => { socket.value.once('up_file_fail', (res) => {
if(multipleFlag) return if (multipleFlag) return
console.log('=========服务端上传至客户端上传失败❌=========') $message.error(res)
// console.log('up_file_fail:', res) handleRefresh()
this.$message.error(res) resetFileStatusFlag()
this.handleRefresh()
this.resetFileStatusFlag()
multipleFlag = true multipleFlag = true
reject() reject()
}) })
} catch (err) { } catch (err) {
reject(err) reject(err)
let errMsg = `上传失败, ${ err }` const errMsg = `上传失败, ${err}`
console.error(errMsg) $message.error(errMsg)
this.$message.error(errMsg) handleRefresh()
this.handleRefresh() resetFileStatusFlag()
this.resetFileStatusFlag()
} }
}) })
} }
reader.readAsArrayBuffer(file) reader.readAsArrayBuffer(file)
}) })
}, }
resetFileStatusFlag() {
this.upFileProgress = 0 const resetFileStatusFlag = () => {
this.curUploadFileName = '' upFileProgress.value = 0
this.showFileProgress = false curUploadFileName.value = ''
this.childDirLoading = false showFileProgress.value = false
}, childDirLoading.value = false
uploadSliceFile(fileInfo) { }
const uploadSliceFile = (fileInfo) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.socket.emit('up_file_slice', fileInfo) socket.value.emit('up_file_slice', fileInfo)
this.socket.once('up_file_slice_success', () => { socket.value.once('up_file_slice_success', () => {
resolve() resolve()
}) })
this.socket.once('up_file_slice_fail', () => { socket.value.once('up_file_slice_fail', () => {
reject('分片文件上传失败') reject('分片文件上传失败')
}) })
this.socket.once('not_exists_dir', (errMsg) => { socket.value.once('not_exists_dir', (errMsg) => {
reject(errMsg) reject(errMsg)
}) })
}) })
}, }
openDir() {
this.childDirLoading = true const openDir = () => {
this.curTarget = null childDirLoading.value = true
this.socket.emit('open_dir', this.curPath) curTarget.value = null
}, socket.value.emit('open_dir', curPath.value)
getPath(name = '') { }
return this.curPath.length === 1 ? `/${ name }` : `${ this.curPath }/${ name }`
}, const getPath = (name = '') => {
adjustHeight() { return curPath.value.length === 1 ? `/${name}` : `${curPath.value}/${name}`
}
const adjustHeight = () => {
let startAdjust = false let startAdjust = false
let timer = null let timer = null
this.$nextTick(() => { $nextTick(() => {
let sftpHeight = localStorage.getItem('sftpHeight') let sftpHeight = localStorage.getItem('sftpHeight')
if(sftpHeight) document.querySelector('.sftp-container').style.height = sftpHeight if (sftpHeight) document.querySelector('.sftp-container').style.height = sftpHeight
else document.querySelector('.sftp-container').style.height = '33vh' // 1/3 else document.querySelector('.sftp-container').style.height = '33vh'
this.$refs['adjust'].addEventListener('mousedown', () => { adjustRef.value.addEventListener('mousedown', () => {
// console.log('')
startAdjust = true startAdjust = true
}) })
document.addEventListener('mousemove', (e) => { document.addEventListener('mousemove', (e) => {
if(!startAdjust) return if (!startAdjust) return
if(timer) clearTimeout(timer) if (timer) clearTimeout(timer)
timer = setTimeout(() => { timer = setTimeout(() => {
sftpHeight = `calc(100vh - ${ e.pageY }px)` sftpHeight = `calc(100vh - ${e.pageY}px)`
document.querySelector('.sftp-container').style.height = sftpHeight document.querySelector('.sftp-container').style.height = sftpHeight
this.$emit('resize') emit('resize')
}) })
}) })
document.addEventListener('mouseup', (e) => { document.addEventListener('mouseup', (e) => {
if(!startAdjust) return if (!startAdjust) return
startAdjust = false startAdjust = false
sftpHeight = `calc(100vh - ${ e.pageY }px)` sftpHeight = `calc(100vh - ${e.pageY}px)`
localStorage.setItem('sftpHeight', sftpHeight) localStorage.setItem('sftpHeight', sftpHeight)
}) })
}) })
}
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.sftp-container { .sftp-container {
position: relative; position: relative;

View File

@ -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,9 +22,9 @@ 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
@ -31,96 +32,93 @@ export default {
host: { host: {
required: true, required: true,
type: String type: String
},
tabKey: {
required: true,
type: String
} }
}, })
data() {
return { const socket = ref(null)
socket: null, const term = ref(null)
term: null, const command = ref('')
command: '', const timer = ref(null)
timer: null, const fitAddon = ref(null)
fitAddon: null, const searchBar = ref(null)
searchBar: null, const isManual = ref(false)
isManual: false // socket const terminalRefs = ref(null)
}
}, const tabKey = ref(props.tabKey)
async mounted() {
this.createLocalTerminal() const getCommand = async () => {
await this.getCommand() let { data } = await $api.getCommand(props.host)
this.connectIO() if (data) command.value = data
}, }
beforeUnmount() {
// this.term.dispose() // const connectIO = () => {
this.isManual = true const { host, token } = props
this.socket?.close() // socket socket.value = io($serviceURI, {
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', path: '/terminal',
forceNew: false, // forceNew: false,
reconnectionAttempts: 1 // reconnectionAttempts: 1
}) })
this.socket.on('connect', () => {
console.log('/terminal socket已连接', this.socket.id) socket.value.on('connect', () => {
// console.log('/terminal socket已连接', socket.value.id)
this.socket.emit('create', { host, token }) socket.value.emit('create', { host, token })
this.socket.on('connect_success', () => { socket.value.on('connect_success', () => {
this.onData() // onData()
this.socket.on('connect_terminal', () => { socket.value.on('connect_terminal', () => {
this.onResize() // () onResize()
this.onFindText() // onFindText()
this.onWebLinks() // link onWebLinks()
if(this.command) this.socket.emit('input', this.command + '\n') if (command.value) socket.value.emit('input', command.value + '\n')
}) })
}) })
this.socket.on('create_fail', (message) => { socket.value.on('create_fail', (message) => {
console.error(message) console.error(message)
this.$notification({ $notification({
title: '创建失败', title: '创建失败',
message, message,
type: 'error' type: 'error'
}) })
}) })
this.socket.on('token_verify_fail', () => { socket.value.on('token_verify_fail', () => {
this.$notification({ $notification({
title: 'Error', title: 'Error',
message: 'token校验失败请重新登录', message: 'token校验失败请重新登录',
type: 'error' type: 'error'
}) })
this.$router.push('/login') $router.push('/login')
}) })
this.socket.on('connect_fail', (message) => { socket.value.on('connect_fail', (message) => {
console.error(message) console.error(message)
this.$notification({ $notification({
title: '连接失败', title: '连接失败',
message, message,
type: 'error' type: 'error'
}) })
}) })
}) })
this.socket.on('disconnect', () => {
socket.value.on('disconnect', () => {
console.warn('terminal websocket 连接断开') console.warn('terminal websocket 连接断开')
if(!this.isManual) this.reConnect() if (!isManual.value) reConnect()
}) })
this.socket.on('connect_error', (err) => {
socket.value.on('connect_error', (err) => {
console.error('terminal websocket 连接错误:', err) console.error('terminal websocket 连接错误:', err)
this.$notification({ $notification({
title: '终端连接失败', title: '终端连接失败',
message: '请检查socket服务是否正常', message: '请检查socket服务是否正常',
type: 'error' type: 'error'
}) })
}) })
}, }
reConnect() {
this.socket.close && this.socket.close() const reConnect = () => {
this.$messageBox.alert( socket.value.close && socket.value.close()
$messageBox.alert(
'<strong>终端连接断开</strong>', '<strong>终端连接断开</strong>',
'Error', 'Error',
{ {
@ -128,135 +126,138 @@ export default {
confirmButtonText: '刷新页面' confirmButtonText: '刷新页面'
} }
).then(() => { ).then(() => {
// this.fitAddon && this.fitAddon.dispose()
// this.term && this.term.dispose()
// this.connectIO()
location.reload() location.reload()
}) })
}, }
createLocalTerminal() {
// https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/ const createLocalTerminal = () => {
let term = new Terminal({ let terminal = new Terminal({
rendererType: 'dom', // canvas dom rendererType: 'dom',
bellStyle: 'sound', bellStyle: 'sound',
// bellSound: './tip.mp3', convertEol: true,
convertEol: true, // cursorBlink: true,
cursorBlink: true, // disableStdin: false,
disableStdin: false, //
fontSize: 18, fontSize: 18,
minimumContrastRatio: 7, // minimumContrastRatio: 7,
theme: { theme: {
foreground: '#ECECEC', // foreground: '#ECECEC',
background: '#000000', // background: '#000000',
cursor: 'help', // cursor: 'help',
selection: '#ff9900', // selection: '#ff9900',
lineHeight: 20 lineHeight: 20
} }
}) })
this.term = term term.value = terminal
term.open(this.$refs['terminal']) terminal.open(terminalRefs.value)
term.writeln('\x1b[1;32mWelcome to EasyNode terminal\x1b[0m.') terminal.writeln('\x1b[1;32mWelcome to EasyNode terminal\x1b[0m.')
term.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.') terminal.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.')
// $ terminal.focus()
// term.prompt = () => { onSelectionChange()
// term.write('\r\n\x1b[33m$ \x1b[0m ') }
// }
term.focus() const onResize = () => {
this.onSelectionChange() fitAddon.value = new FitAddon()
}, term.value.loadAddon(fitAddon.value)
onResize() { fitAddon.value.fit()
this.fitAddon = new FitAddon() let { rows, cols } = term.value
this.term.loadAddon(this.fitAddon) socket.value.emit('resize', { rows, cols })
this.fitAddon.fit() window.addEventListener('resize', handleResize)
let { rows, cols } = this.term }
this.socket.emit('resize', { rows, cols }) // fitresize
window.addEventListener('resize', this.handleResize) const handleResize = () => {
}, if (timer.value) clearTimeout(timer.value)
handleResize() { timer.value = setTimeout(() => {
if(this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => {
let temp = [] let temp = []
let panes= Array.from(document.getElementsByClassName('el-tab-pane')) let panes = Array.from(document.getElementsByClassName('el-tab-pane'))
// block
panes.forEach((item, index) => { panes.forEach((item, index) => {
temp[index] = item.style.display temp[index] = item.style.display
item.style.display = 'block' item.style.display = 'block'
}) })
this.fitAddon?.fit() // (element tabdisplay:nonetab) fitAddon.value?.fit()
//
panes.forEach((item, index) => { panes.forEach((item, index) => {
item.style.display = temp[index] item.style.display = temp[index]
}) })
let { rows, cols } = this.term let { rows, cols } = term.value
// console.log('resize: ', { rows, cols }) socket.value?.emit('resize', { rows, cols })
this.socket?.emit('resize', { rows, cols }) focusTab()
}, 200) }, 200)
}, }
onWebLinks() {
this.term.loadAddon(new WebLinksAddon()) const onWebLinks = () => {
}, term.value.loadAddon(new WebLinksAddon())
onFindText() { }
const onFindText = () => {
const searchAddon = new SearchAddon() const searchAddon = new SearchAddon()
this.searchBar = new SearchBarAddon({ searchAddon }) searchBar.value = new SearchBarAddon({ searchAddon })
this.term.loadAddon(searchAddon) term.value.loadAddon(searchAddon)
// searchAddon.findNext('SSH', { decorations: { activeMatchBackground: '#ff0000' } }) term.value.loadAddon(searchBar.value)
this.term.loadAddon(this.searchBar) }
// this.searchBar.show()
}, const onSelectionChange = () => {
onSelectionChange() { term.value.onSelectionChange(() => {
this.term.onSelectionChange(() => { let str = term.value.getSelection()
let str = this.term.getSelection() if (!str) return
if(!str) return const text = new Blob([str], { type: 'text/plain' })
const text = new Blob([str,], { type: 'text/plain' })
// eslint-disable-next-line no-undef
const item = new ClipboardItem({ const item = new ClipboardItem({
'text/plain': text 'text/plain': text
}) })
navigator.clipboard.write([item,]) 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() // codePointAtASCII
// // 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 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;

View File

@ -1,179 +1,149 @@
<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() {
return this.terminalTabs.length > 1
}
},
watch: {
showInputCommand(newVal) {
if(!newVal) this.$refs['info-side'].inputCommandStatus = false
}
},
created() {
if (!this.token) return this.$router.push('login')
let { host, name } = this.$route.query
this.name = name
this.host = host
document.title = `${ document.title }-${ name }`
let key = Date.now().toString() let key = Date.now().toString()
this.terminalTabs.push({ title: name, key }) terminalTabs.push({ title: routeName, key })
this.activeTab = key activeTab.value = key
this.registryDbClick() registryDbClick()
}, })
// mounted() {
// window.onbeforeunload = () => { // const windowBeforeUnload = () => {
// return '' // window.onbeforeunload = () => {
// } // return ''
// }, // }
methods: { // }
connectSftp(flag) {
this.showSftp = flag const connectSftp = (flag) => {
this.resizeTerminal() showSftp.value = flag
}, resizeTerminal()
clickInputComand() { }
this.showInputCommand = true
}, const clickInputCommand = () => {
tabAdd() { showInputCommand.value = true
if(this.timer) clearTimeout(this.timer) }
this.timer = setTimeout(() => {
let { name } = this const tabAdd = () => {
let title = name 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, key }) terminalTabs.push({ title, key })
this.activeTab = key activeTab.value = key
this.registryDbClick() tabChange(key)
registryDbClick()
}, 200) }, 200)
}, }
removeTab(removeKey) {
let idx = this.terminalTabs.findIndex(({ key }) => removeKey === key) const removeTab = (removeKey) => {
this.terminalTabs.splice(idx, 1) let idx = terminalTabs.findIndex(({ key }) => removeKey === key)
if(removeKey !== this.activeTab) return terminalTabs.splice(idx, 1)
this.activeTab = this.terminalTabs[0].key if (removeKey !== activeTab.value) return
}, activeTab.value = terminalTabs[0].key
tabChange(key) { }
this.$refs[key][0].focusTab()
}, const tabChange = async (key) => {
handleFullScreen() { await $nextTick()
if(this.isFullScreen) document.exitFullscreen() const curTabTerminal = terminalTabRefs.value.find(({ tabKey }) => key === tabKey)
curTabTerminal?.focusTab()
}
const handleFullScreen = () => {
if (isFullScreen.value) document.exitFullscreen()
else document.getElementsByClassName('terminals')[0].requestFullscreen() else document.getElementsByClassName('terminals')[0].requestFullscreen()
this.isFullScreen = !this.isFullScreen isFullScreen.value = !isFullScreen.value
}, }
registryDbClick() {
this.$nextTick(() => { const registryDbClick = () => {
$nextTick(() => {
let tabItems = Array.from(document.getElementsByClassName('el-tabs__item')) let tabItems = Array.from(document.getElementsByClassName('el-tabs__item'))
tabItems.forEach(item => { tabItems.forEach(item => {
item.removeEventListener('dblclick', this.handleDblclick) item.removeEventListener('dblclick', handleDblclick)
item.addEventListener('dblclick', this.handleDblclick) item.addEventListener('dblclick', handleDblclick)
}) })
}) })
}, }
handleDblclick(e) {
if(this.terminalTabs.length > 1) { const handleDblclick = (e) => {
if (terminalTabs.length > 1) {
let key = e.target.id.substring(4) let key = e.target.id.substring(4)
// console.log('dblclick', key) // console.log('dblclick', key)
this.removeTab(key) removeTab(key)
} }
}, }
handleVisibleSidebar() {
this.visible = !this.visible const handleVisibleSidebar = () => {
this.resizeTerminal() visible.value = !visible.value
}, resizeTerminal()
resizeTerminal() { }
let terminals = this.$refs
for(let terminal in terminals) { const resizeTerminal = () => {
const { handleResize } = this.$refs[terminal][0] || {} for (let terminalTabRef of terminalTabRefs.value) {
const { handleResize } = terminalTabRef || {}
handleResize && handleResize() handleResize && handleResize()
} }
}, }
handleInputCommand(command) {
// console.log(command) const handleInputCommand = async (command) => {
this.$refs[this.activeTab][0].handleInputCommand(`${ command }\n`) const curTabTerminal = terminalTabRefs.value.find(({ tabKey }) => activeTab.value === tabKey)
this.showInputCommand = false await $nextTick()
} curTabTerminal?.focusTab()
} curTabTerminal.handleInputCommand(`${command}\n`)
showInputCommand.value = false
} }
</script> </script>
@ -181,15 +151,18 @@ export default {
.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>