✨ 增强脚本输入功能&优化与修复若干项

This commit is contained in:
chaos-zhu
2025-03-20 00:57:54 +08:00
parent ab3d98da32
commit def573c06b
35 changed files with 1500 additions and 352 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "3.0.3",
"version": "3.0.4",
"description": "easynode-web",
"private": true,
"scripts": {
+12
View File
@@ -118,6 +118,18 @@ export default {
batchRemoveScript(data) {
return axios({ url: '/batch-remove-script', method: 'post', data })
},
getScriptGroupList() {
return axios({ url: '/script-group', method: 'get' })
},
addScriptGroup(data) {
return axios({ url: '/script-group', method: 'post', data })
},
updateScriptGroup(id, data) {
return axios({ url: `/script-group/${ id }`, method: 'put', data })
},
deleteScriptGroup(id) {
return axios({ url: `/script-group/${ id }`, method: 'delete' })
},
getOnekeyRecord() {
return axios({ url: '/onekey', method: 'get' })
},
+5 -36
View File
@@ -1,37 +1,6 @@
// $--colors: (
// "primary": (
// "base": green,
// ),
// "success": (
// "base": #21ba45,
// ),
// "warning": (
// "base": #f2711c,
// ),
// "danger": (
// "base": #db2828,
// ),
// "error": (
// "base": #db2828,
// ),
// "info": (
// "base": #42b8dd,
// ),
// );
// You should use them in scss, because we calculate it by sass.
// comment next lines to use default color
// @forward "element-plus/theme-chalk/src/common/var.scss" with (
// // do not use same name, it will override.
// // $colors: $--colors,
// // $button-padding-horizontal: ("default": 50px)
// );
// if you want to import all
// @use "element-plus/theme-chalk/src/index.scss" as *;
// You can comment it to hide debug info.
// @debug $--colors;
// custom dark variables
@use "./dark.scss";
@use "./dark.scss";
.el-tabs {
--el-tabs-header-height: 32px;
}
+1
View File
@@ -97,6 +97,7 @@ const plusFeatures = [
'服务器跳板机功能,支持任意数量服务器的连续跳板',
'批量修改实例配置(优化版)',
'脚本库批量导出导入',
'脚本库分组与终端工具栏支持',
'凭据管理支持解密带密码保护的密钥',
'通知方式无限制',
'本地socket断开自动重连',
+113 -16
View File
@@ -68,20 +68,31 @@
<p v-if="isNew" class="conspicuous">
新版本可用: {{ latestVersion }} -> <a
class="link"
href="https://github.com/chaos-zhu/easynode/releases"
href="https://github.com/chaos-zhu/easynode?tab=readme-ov-file#%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2"
target="_blank"
>https://github.com/chaos-zhu/easynode/releases</a>
</p>
<p>
功能更新日志:<a
class="link"
href="https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md"
target="_blank"
>https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md</a>
>https://github.com/chaos-zhu/easynode</a>
</p>
<template v-if="features.length > 0 && isNew">
<div class="version_features_title">
<el-icon><Document /></el-icon>
新版本更新了以下内容:
</div>
<ul class="conspicuous feature_list">
<li v-for="feature in features" :key="feature">
{{ feature }}
</li>
</ul>
</template>
<p>
TG更新通知频道:<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
</p>
<p>
项目地址:<a
class="link"
href="https://github.com/chaos-zhu/easynode"
target="_blank"
>https://github.com/chaos-zhu/easynode</a>
</p>
<div class="about_footer">
<el-button type="info" @click="visible = false">关闭</el-button>
</div>
@@ -106,7 +117,7 @@
<script setup>
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { User, Sunny, Moon, Fold } from '@element-plus/icons-vue'
import { User, Sunny, Moon, Fold, Document } from '@element-plus/icons-vue'
import packageJson from '../../package.json'
import MenuList from './menuList.vue'
@@ -116,6 +127,7 @@ const visible = ref(false)
const checkVersionErr = ref(false)
const currentVersion = ref(`v${ packageJson.version }`)
const latestVersion = ref(null)
const features = ref([])
const menuCollapse = ref(false)
const discount = ref(false)
@@ -145,25 +157,22 @@ const gotoPlusPage = () => {
router.push('/setting?tabKey=plus')
}
async function checkLatestVersion() {
const timeout = 3000
async function checkLatestVersionByGitRelease() {
const timeout = 5000
try {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
const url = `https://api.github.com/repos/chaos-zhu/easynode/releases?ts=${ new Date().getTime() }`
const fetchPromise = fetch(url, {
headers: {
'Accept': 'application/vnd.github.v3+json'
}
})
const response = await Promise.race([fetchPromise, timeoutPromise,])
if (!response.ok) {
throw new Error('版本信息请求失败: ' + response.statusText)
}
const releases = await response.json()
// console.log('releases:', releases)
const filteredReleases = releases.filter(release => !release.tag_name.startsWith('client'))
@@ -176,7 +185,48 @@ async function checkLatestVersion() {
}
}
checkLatestVersion()
async function checkLatestVersionByJson() {
const timeout = 5000
try {
let date = new Date()
let lastGetVersionTime = localStorage.getItem('lastGetVersionTime')
// 10分钟内不重复请求
if (lastGetVersionTime && (date.getTime() - Number(lastGetVersionTime) < 1000 * 60 * 10)) {
let latestVersionFromLocal = localStorage.getItem('latestVersion')
let featuresFromLocal = localStorage.getItem('features')
if (latestVersionFromLocal && featuresFromLocal) {
latestVersion.value = latestVersionFromLocal
features.value = JSON.parse(featuresFromLocal)
return
}
}
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
const url = `https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/refs/heads/main/server/version.json?ts=${ new Date().getTime() }`
const fetchPromise = fetch(url)
const response = await Promise.race([fetchPromise, timeoutPromise,])
if (!response.ok) {
throw new Error('版本信息请求失败: ' + response.statusText)
}
const releases = await response.json()
// console.log('releases:', releases)
if (Array.isArray(releases) && releases.length > 0) {
latestVersion.value = releases[0].version
features.value = releases[0].features
localStorage.setItem('lastGetVersionTime', date.getTime())
localStorage.setItem('latestVersion', latestVersion.value)
localStorage.setItem('features', JSON.stringify(features.value))
}
} catch (error) {
checkVersionErr.value = true
console.error('版本信息请求失败:', error.message)
checkLatestVersionByGitRelease()
}
}
checkLatestVersionByJson()
let timer = null
const checkFirstVisit = () => {
@@ -237,6 +287,7 @@ onBeforeUnmount(() => {
.new_version {
color: red;
text-decoration: underline;
}
}
@@ -299,6 +350,52 @@ onBeforeUnmount(() => {
color: #F56C6C;
font-weight: 500;
}
.feature_list {
li {
list-style: none;
margin-left: 20px;
padding: 6px 0;
line-height: 1.5;
position: relative;
counter-increment: feature-counter;
&::before {
content: counter(feature-counter);
position: absolute;
left: -25px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
background: #F56C6C;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
&:not(:last-child) {
border-bottom: 1px dashed rgba(245, 108, 108, 0.2);
}
}
}
.version_features_title {
display: flex;
align-items: center;
color: #F56C6C;
font-weight: 500;
margin: 15px 0 10px;
padding-bottom: 10px;
border-bottom: 2px solid rgba(245, 108, 108, 0.2);
.el-icon {
margin-right: 6px;
font-size: 16px;
}
}
}
.about_footer {
+6
View File
@@ -15,6 +15,7 @@ const useStore = defineStore({
groupList: [],
sshList: [],
scriptList: [],
scriptGroupList: [],
localScriptList: [],
HostStatusSocket: null,
user: localStorage.getItem('user') || null,
@@ -71,6 +72,7 @@ const useStore = defineStore({
await this.getHostList()
await this.getSSHList()
await this.getScriptList()
await this.getScriptGroupList()
await this.getPlusInfo()
this.wsClientsStatus()
},
@@ -96,6 +98,10 @@ const useStore = defineStore({
const { data: scriptList } = await $api.getScriptList()
this.$patch({ scriptList })
},
async getScriptGroupList() {
const { data: scriptGroupList } = await $api.getScriptGroupList()
this.$patch({ scriptGroupList })
},
async getLocalScriptList() {
const { data: localScriptList } = await $api.getLocalScriptList()
this.$patch({ localScriptList })
+1 -1
View File
@@ -238,7 +238,7 @@ const handleDecryptKey = async () => {
<style lang="scss" scoped>
.credentials_container {
padding: 20px;
padding: 0 20px 20px 20px;
.header {
padding: 15px;
display: flex;
+3 -2
View File
@@ -120,6 +120,8 @@ const list = computed(() => {
let addGroup = () => {
groupForm.id = null
const maxIndex = Math.max(...groupList.value.map(item => item.index), 0)
groupForm.index = maxIndex + 1
groupFormVisible.value = true
isModify.value = false
}
@@ -167,10 +169,9 @@ const deleteGroup = ({ id, name }) => {
<style lang="scss" scoped>
.group_container {
padding: 20px;
padding:0 20px 20px 20px;
.header {
padding: 15px;
display: flex;
align-items: center;
justify-content: end;
+1 -1
View File
@@ -444,7 +444,7 @@ onActivated(async () => {
<style lang="scss" scoped>
.onekey_container {
padding: 20px;
padding: 0 20px 20px 20px;
.header {
padding: 15px;
display: flex;
@@ -0,0 +1,192 @@
<template>
<el-dialog
v-model="visible"
width="600px"
top="150px"
:title="isModify ? '修改脚本' : '添加脚本'"
:close-on-click-modal="false"
@open="handleOpen"
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
:hide-required-asterisk="true"
label-suffix=":"
label-width="100px"
:show-message="false"
>
<el-form-item key="group" label="分组" prop="group">
<el-select
v-model="formData.group"
placeholder=""
clearable
style="width: 100%;"
>
<el-option
v-for="item in groupList"
:key="item.id"
:label="item.name"
:value="item.id"
:disabled="item.id === 'builtin'"
/>
</el-select>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input
v-model="formData.name"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item>
<!-- <el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item> -->
<el-form-item label="序号" prop="index">
<el-input
v-model.trim.number="formData.index"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="command" label="内容">
<el-input
v-model="formData.command"
type="textarea"
:rows="5"
clearable
autocomplete="off"
style="margin-top: 5px;"
placeholder="shell script"
/>
</el-form-item>
</el-form>
<template #footer>
<span>
<el-button @click="visible = false">关闭</el-button>
<el-button type="primary" @click="handleSubmit">{{ isModify ? '修改' : '添加' }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, reactive, watch } from 'vue'
import { getCurrentInstance } from 'vue'
const props = defineProps({
show: {
type: Boolean,
default: false
},
defaultData: {
type: Object,
default: () => ({})
},
defaultScript: {
type: String,
default: ''
},
defaultGroup: {
type: String,
default: 'default'
}
})
const emit = defineEmits(['update:show', 'success',])
const { proxy: { $api, $message, $store } } = getCurrentInstance()
const visible = computed({
get: () => props.show,
set: (val) => emit('update:show', val)
})
const formRef = ref(null)
const groupList = computed(() => $store.scriptGroupList || [])
const scriptList = computed(() => $store.scriptList)
const curGroupScripts = computed(() => scriptList.value.filter(item => item.group === props.defaultGroup))
const nextIndex = computed(() => curGroupScripts.value.reduce((acc, cur) => Math.max(acc, Number(cur.index) || 0), 0) + 1)
const formData = reactive({
group: '',
name: '',
description: '',
index: nextIndex.value || 0,
command: ''
})
const isModify = computed(() => Boolean(formData.id))
const rules = {
group: { required: true, message: '选择一个分组' },
name: { required: true, trigger: 'change' },
description: { required: false, trigger: 'change' },
index: { required: false, type: 'number', trigger: 'change' },
command: { required: true, trigger: 'change' }
}
watch(() => props.defaultData, (newVal) => {
if (newVal?.id) {
Object.assign(formData, { ...newVal })
}
}, { immediate: true, deep: true })
watch(() => props.defaultScript, (newVal) => {
if (newVal && !formData.id) {
formData.command = newVal
}
}, { immediate: true })
watch(() => props.defaultGroup, (newVal) => {
if (newVal && !formData.id) {
if (newVal === 'builtin') {
formData.group = 'default'
} else {
formData.group = newVal
}
}
}, { immediate: true })
const handleClose = () => {
formRef.value?.resetFields()
Object.assign(formData, {
id: null,
group: props.defaultGroup,
name: '',
description: '',
index: nextIndex.value,
command: props.defaultScript || ''
})
}
const handleOpen = () => {
if (!formData.id) {
formData.index = nextIndex.value
}
}
const handleSubmit = () => {
formRef.value.validate()
.then(async () => {
const data = { ...formData }
if (isModify.value) {
await $api.updateScript(data.id, data)
} else {
await $api.addScript(data)
}
visible.value = false
await $store.getScriptList()
emit('success')
$message.success('success')
})
}
</script>
@@ -0,0 +1,204 @@
<template>
<el-dialog
v-model="visible"
width="600px"
:top="isMobile() ? '45px' : '15vh'"
:append-to-body="false"
title="脚本分组管理"
:close-on-click-modal="false"
>
<div class="group_container">
<div class="header">
<el-button type="primary" @click="addGroup">添加分组</el-button>
</div>
<el-table v-loading="loading" :data="list">
<el-table-column prop="index" label="序号" />
<el-table-column prop="name" label="分组名称" />
<el-table-column label="关联脚本数量" min-width="115px">
<template #default="{ row }">
<el-popover
v-if="row.scripts.list.length !== 0"
placement="left"
:width="350"
trigger="hover"
>
<template #reference>
<u class="script_count">{{ row.scripts.count }}</u>
</template>
<ul>
<li v-for="item in row.scripts.list" :key="item.id">
<span>{{ item.name }}</span>
-
<span>{{ item.description }}</span>
</li>
</ul>
</el-popover>
<u v-else class="script_count">0</u>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="160px">
<template #default="{ row }">
<template v-if="row.id !== 'builtin'">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'default'" type="danger" @click="deleteGroup(row)">删除</el-button>
</template>
<template v-else>
<span>--</span>
</template>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="groupFormVisible"
width="600px"
top="150px"
:title="isModify ? '修改分组' : '添加分组'"
:close-on-click-modal="false"
@close="clearFormInfo"
>
<el-form
ref="updateFormRef"
:model="groupForm"
:rules="rules"
label-width="100px"
>
<el-form-item label="分组名称" prop="name">
<el-input v-model="groupForm.name" />
</el-form-item>
<el-form-item label="序号" prop="index">
<el-input v-model.number="groupForm.index" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="groupFormVisible = false">取消</el-button>
<el-button type="primary" @click="updateForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</el-dialog>
</template>
<script setup>
import { ref, computed, getCurrentInstance, nextTick } from 'vue'
import { isMobile } from '@/utils'
const props = defineProps({
show: {
required: true,
type: Boolean
}
})
const emit = defineEmits(['update:show', 'group-deleted',])
const visible = computed({
get: () => props.show,
set: (newVal) => emit('update:show', newVal)
})
const { proxy: { $api, $store, $message, $messageBox } } = getCurrentInstance()
const loading = ref(false)
const groupFormVisible = ref(false)
const isModify = ref(false)
const updateFormRef = ref()
const groupForm = ref({
id: null,
name: '',
index: 0
})
const rules = {
name: [{ required: true, message: '请输入分组名称', trigger: 'blur' },],
index: [{ type: 'number', message: '序号必须为数字值' },]
}
const scriptList = computed(() => $store.scriptList)
const groupList = computed(() => $store.scriptGroupList)
const list = computed(() => {
return groupList.value.map(item => {
const scripts = scriptList.value.reduce((prev, next) => {
if (next.group === item.id) {
prev.count++
prev.list.push(next)
}
return prev
}, { count: 0, list: [] })
return { ...item, scripts }
})
})
const addGroup = () => {
groupForm.value.id = null
groupForm.value.index = groupList.value.reduce((max, group) =>
Math.max(max, Number(group.index) || 0), 0) + 1
groupFormVisible.value = true
isModify.value = false
}
const handleChange = (row) => {
Object.assign(groupForm.value, { ...row })
groupFormVisible.value = true
isModify.value = true
}
const updateForm = () => {
updateFormRef.value.validate()
.then(async () => {
const { id, index, name } = groupForm.value
if (isModify.value) {
await $api.updateScriptGroup(id, { index, name })
} else {
await $api.addScriptGroup({ index, name })
}
await $store.getScriptGroupList()
groupFormVisible.value = false
$message.success('success')
})
}
const clearFormInfo = () => {
nextTick(() => updateFormRef.value.resetFields())
}
const deleteGroup = ({ id, name }) => {
$messageBox.confirm(`确认删除分组:${ name } (分组下脚本将移动至默认分组)`, 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await $api.deleteScriptGroup(id)
await $store.getScriptList()
await $store.getScriptGroupList()
emit('group-deleted', id)
$message.success('success')
})
}
</script>
<style lang="scss" scoped>
.group_container {
padding: 0 20px 20px 20px;
.header {
display: flex;
align-items: center;
justify-content: end;
}
}
.script_count {
display: block;
width: 100px;
text-align: center;
font-size: 15px;
color: #87cf63;
cursor: pointer;
}
</style>
+107 -147
View File
@@ -15,12 +15,11 @@
<el-button
v-show="selectScripts.length"
type="danger"
class="batch_remove_btn"
@click="handleBatchRemove"
>
批量删除
</el-button>
<el-button type="primary" class="add_script_btn" @click="addScript">添加脚本</el-button>
<el-button type="primary" @click="addScript">添加脚本</el-button>
<PlusSupportTip>
<el-dropdown trigger="click" :disabled="!isPlusActive">
<el-button type="primary" class="group_action_btn" :disabled="!isPlusActive">
@@ -34,126 +33,98 @@
</template>
</el-dropdown>
</PlusSupportTip>
</div>
<el-table
v-loading="loading"
:data="paginatedFilteredList"
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
width="55"
:selectable="(row) => {
return row.index !== '--' && row.index !== '-' && row.index !== undefined && row.index !== null
}"
/>
<el-table-column prop="index" label="序号" width="100px" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="description" label="描述" />
<el-table-column prop="command" label="指令内容" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" width="160px">
<template #default="{ row }">
<template v-if="row.index !== '--'">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'own'" type="danger" @click="handleRemove(row)">删除</el-button>
</template>
<span v-else>--</span>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]"
:total="scriptList.length"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<PlusSupportTip>
<el-button
type="primary"
class="group_action_btn"
:disabled="!isPlusActive"
@click="ScriptGroupVisible = true"
>
分组管理
</el-button>
</PlusSupportTip>
</div>
<el-dialog
v-model="formVisible"
width="600px"
top="150px"
:title="isModify ? '修改脚本' : '添加脚本'"
:close-on-click-modal="false"
@close="clearFormInfo"
>
<el-form
ref="updateFormRef"
:model="formData"
:rules="rules"
:hide-required-asterisk="true"
label-suffix=":"
label-width="100px"
:show-message="false"
<el-tabs v-model="activeTab" type="border-card" class="script-tabs">
<el-tab-pane
v-for="group in groupList"
:key="group.id"
:label="group.name"
:name="group.id"
>
<el-form-item label="名称" prop="name">
<el-input
v-model="formData.name"
clearable
placeholder=""
autocomplete="off"
<el-table
v-loading="loading"
:data="getFilteredScriptsByGroup(group.id)"
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
width="55"
:selectable="(row) => {
return row.index !== '--' && row.index !== '-' && row.index !== undefined && row.index !== null
}"
/>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
clearable
placeholder=""
autocomplete="off"
<el-table-column prop="index" label="序号" width="100px" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="description" label="描述" />
<el-table-column prop="command" label="指令内容" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" width="160px">
<template #default="{ row }">
<template v-if="row.index !== '--'">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'own'" type="danger" @click="handleRemove(row)">删除</el-button>
</template>
<span v-else>--</span>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]"
:total="getFilteredScriptsByGroup(group.id).length"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-form-item>
<el-form-item label="序号" prop="index">
<el-input
v-model.trim.number="formData.index"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="command" label="内容">
<el-input
v-model="formData.command"
type="textarea"
:rows="5"
clearable
autocomplete="off"
style="margin-top: 5px;"
placeholder="shell script"
/>
</el-form-item>
</el-form>
<template #footer>
<span>
<el-button @click="formVisible = false">关闭</el-button>
<el-button type="primary" @click="updateForm">{{ isModify ? '修改' : '添加' }}</el-button>
</span>
</template>
</el-dialog>
</div>
</el-tab-pane>
</el-tabs>
<ScriptEdit
v-model:show="formVisible"
:default-data="currentScript"
:default-group="activeTab"
@success="handleEditSuccess"
/>
<ImportScript
v-model:show="importVisible"
@update-list="() => $store.getScriptList()"
/>
<ScriptGroup
v-model:show="ScriptGroupVisible"
@group-deleted="handleGroupDeleted"
/>
</div>
</template>
<script setup>
import { ref, reactive, computed, nextTick, getCurrentInstance, h } from 'vue'
import { ref, reactive, computed, nextTick, getCurrentInstance, h, watch } from 'vue'
import ImportScript from './components/import-script.vue'
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
import { ArrowDown, Search } from '@element-plus/icons-vue'
import { exportFile } from '@/utils'
import ScriptGroup from './components/script-group.vue'
import ScriptEdit from './components/script-edit.vue'
const { proxy: { $api, $message, $messageBox, $store, $tools } } = getCurrentInstance()
const loading = ref(false)
const formVisible = ref(false)
let isModify = ref(false)
const selectScripts = ref([])
const handleSelectionChange = (val) => {
selectScripts.value = val
@@ -173,57 +144,21 @@ const handleBatchRemove = () => {
})
}
let formData = reactive({
name: '',
description: '',
index: 0,
command: ''
})
const rules = computed(() => {
return {
name: { required: true, trigger: 'change' },
description: { required: false, trigger: 'change' },
index: { required: false, type: 'number', trigger: 'change' },
command: { required: true, trigger: 'change' }
}
})
const updateFormRef = ref(null)
const currentScript = ref({})
const scriptList = computed(() => $store.scriptList)
const isPlusActive = computed(() => $store.isPlusActive)
let addScript = () => {
formData.id = null
formData.index = scriptList.value.reduce((acc, cur) => Math.max(acc, Number(cur.index) || 0), 0) + 1
isModify.value = false
const groupList = computed(() => $store.scriptGroupList || [])
const addScript = () => {
currentScript.value = {}
formVisible.value = true
}
const handleChange = (row) => {
Object.assign(formData, { ...row })
currentScript.value = { ...row }
formVisible.value = true
isModify.value = true
}
function updateForm() {
updateFormRef.value.validate()
.then(async () => {
let data = { ...formData }
if (isModify.value) {
await $api.updateScript(data.id, data)
} else {
await $api.addScript(data)
}
formVisible.value = false
await $store.getScriptList()
$message.success('success')
})
}
const clearFormInfo = () => {
nextTick(() => updateFormRef.value.resetFields())
}
const handleRemove = ({ id, name }) => {
@@ -263,11 +198,14 @@ const filteredScriptList = computed(() => {
)
})
const paginatedFilteredList = computed(() => {
const activeTab = ref(computed(() => groupList.value?.[0]?.id || 'default').value)
const getFilteredScriptsByGroup = (groupId) => {
const groupScripts = filteredScriptList.value.filter(script => script.group === groupId)
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredScriptList.value.slice(start, end)
})
return groupScripts.slice(start, end)
}
const handleSizeChange = (val) => {
pageSize.value = val
@@ -282,24 +220,46 @@ const handleSearch = () => {
currentPage.value = 1
}
const ScriptGroupVisible = ref(false)
const handleGroupDeleted = (deletedGroupId) => {
if (deletedGroupId === activeTab.value) {
nextTick(() => {
activeTab.value = groupList.value?.[0]?.id || 'default'
})
}
}
watch(activeTab, () => {
currentPage.value = 1
})
const handleEditSuccess = () => {
currentScript.value = {}
}
</script>
<style lang="scss" scoped>
.scripts_container {
padding: 20px;
padding: 0 20px 20px 20px;
.header {
padding: 15px;
display: flex;
align-items: center;
justify-content: end;
.add_script_btn {
margin: 0 10px;
.group_action_btn {
margin-left: 10px;
}
.search_input {
width: 300px;
margin-right: auto;
}
}
.script-tabs {
margin-top: 20px;
}
}
.host_count {
+3 -2
View File
@@ -2,9 +2,9 @@
<el-dialog
v-model="visible"
width="600px"
top="45px"
:top="isMobile() ? '45px' : '15vh'"
:append-to-body="false"
title="分组管理"
title="实例分组管理"
:close-on-click-modal="false"
>
<Group />
@@ -14,6 +14,7 @@
<script setup>
import { computed } from 'vue'
import Group from '@/views/group/index.vue'
import { isMobile } from '@/utils'
const props = defineProps({
show: {
@@ -382,7 +382,12 @@ let confHostList = computed(() => {
})
const setDefaultData = () => {
if (!defaultData.value) return
if (!defaultData.value) {
// 添加新实例时,设置index为当前最大index + 1
const maxIndex = Math.max(...hostList.value.map(host => host.index || 0), 0)
hostForm.value.index = maxIndex + 1
return
}
// eslint-disable-next-line no-unused-vars
let { id, ...rest } = defaultData.value
for (let [key,] of Object.entries(hostForm.value)) {
@@ -1,30 +0,0 @@
<template>
<div class="command_history_container">
<ul>
<li
v-for="item in list"
:key="item"
>
{{ item }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
defineProps({
list: {
required: true,
type: Array
}
})
</script>
<style lang="scss" scoped>
.command_history_container {
}
</style>
@@ -0,0 +1,101 @@
<template>
<div ref="footerBarContainerRef" class="footer_bar_container">
<div ref="adjustRef" class="adjust" />
<div class="footer_bar_content">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="文件传输" name="sftp">
<Sftp :host-id="hostId" />
</el-tab-pane>
<el-tab-pane label="脚本库" name="script">
<ScriptInput :host-id="hostId" @exec-command="execCommand" />
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { EventBus } from '@/utils'
import Sftp from './sftp.vue'
import ScriptInput from './script-input.vue'
defineProps({
hostId: {
required: true,
type: String
}
})
const emit = defineEmits(['resize', 'exec-command',])
const footerBarContainerRef = ref(null)
const adjustRef = ref(null)
const activeTab = ref('sftp')
const adjustHeight = async () => {
let startAdjust = false
let timer = null
await nextTick()
try {
let footerBarHeight = localStorage.getItem('footerBarHeight')
if (footerBarHeight) footerBarContainerRef.value.style.height = footerBarHeight
adjustRef.value.addEventListener('mousedown', () => {
startAdjust = true
})
document.addEventListener('mousemove', (e) => {
if (!startAdjust) return
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
footerBarHeight = `calc(100vh - ${ e.pageY }px - 20px)`
footerBarContainerRef.value.style.height = footerBarHeight
emit('resize')
})
})
document.addEventListener('mouseup', () => {
if (!startAdjust) return
startAdjust = false
localStorage.setItem('footerBarHeight', footerBarHeight)
EventBus.$emit('update-footer-bar-height')
})
} catch (error) {
console.warn(error.message)
}
}
const execCommand = (command) => {
emit('exec-command', command)
}
onMounted(() => {
adjustHeight()
EventBus.$on('update-footer-bar-height', () => {
adjustHeight()
})
})
</script>
<style lang="scss" scoped>
.footer_bar_container {
position: relative;
background: #ffffff;
border: 1px solid var(--el-border-color);
min-height: 100px;
.adjust {
user-select: none;
position: absolute;
top: -3px;
width: 100%;
height: 5px;
background: var(--el-color-primary);
opacity: 0.3;
cursor: ns-resize;
}
.footer_bar_content {
width: 100%;
height: 100%;
background-color: var(--el-fill-color-light);
}
}
</style>
@@ -0,0 +1,542 @@
<template>
<div class="script_input_container">
<div v-show="!isPlusActive" class="plus_tips">
<div class="plus_tips_content">
<el-icon class="lock-icon"><Lock /></el-icon>
<div class="plus_label">PLUS</div>
<p>此功能仅限PLUS版使用, <span class="to_active" @click="gotoPlusPage">去激活</span></p>
</div>
</div>
<div class="left_box">
<div class="group_list">
<div
v-for="group in scriptGroupList"
:key="group.id"
:class="['group_item', { active: activeGroup === group.id }]"
@click="handleSelectGroup(group)"
>
{{ group.name }}
</div>
<div
class="group_item group_manage"
@click="handleShowGroupManage"
>
<el-icon><Setting /></el-icon>
分组管理
</div>
</div>
<div class="script_list">
<template v-if="filteredScripts.length">
<div
v-for="script in filteredScripts"
:key="script.id"
class="script_item"
>
<div class="script_info">
<span class="script_name">{{ script.name }}:</span>
<span class="script_command">{{ script.command }}</span>
</div>
<div class="hover_icons">
<el-icon
class="action_icon"
title="执行"
@click="handleExecScript(script)"
>
<VideoPlay />
</el-icon>
<el-icon
v-if="activeGroup !== 'builtin'"
class="action_icon"
title="编辑脚本"
@click="handleEditScript(script)"
>
<Edit />
</el-icon>
<el-icon
class="action_icon"
title="发送到脚本编辑器"
@click="handleSendToEditor(script)"
>
<ArrowRightBold />
</el-icon>
</div>
</div>
<div
v-if="activeGroup !== 'builtin'"
class="script_item add_script_btn"
@click="handleAddScript"
>
<el-icon><Plus /></el-icon>
<span>新建脚本</span>
</div>
</template>
<div v-else class="empty-list">
<el-empty
:image-size="60"
description="暂无脚本"
>
<template #description>
<p>当前分组暂无脚本</p>
</template>
<el-button
type="primary"
@click="handleAddScript"
>
添加脚本
</el-button>
</el-empty>
</div>
</div>
</div>
<div class="right_box">
<div class="editor_title">脚本编辑器</div>
<div class="editor_content">
<el-input
v-model="scriptContent"
type="textarea"
:rows="10"
placeholder="请输入脚本内容..."
/>
<div class="action_btn">
<el-button
v-show="scriptContent"
type="primary"
plain
@click="handleSaveAsScript"
>
保存脚本
</el-button>
<el-button
type="primary"
@click="handleSendToTerminal"
>
发送到终端
</el-button>
</div>
</div>
</div>
</div>
<ScriptEdit
v-model:show="scriptEditVisible"
:default-data="editScriptData"
:default-script="scriptContent"
:default-group="activeGroup"
@success="handleScriptEditSuccess"
/>
<ScriptGroup
v-model:show="scriptGroupVisible"
@group-deleted="handleGroupDeleted"
/>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import { useRouter } from 'vue-router'
import { VideoPlay, Edit, Setting, Plus, ArrowRightBold, Lock } from '@element-plus/icons-vue'
import ScriptEdit from '@/views/scripts/components/script-edit.vue'
import ScriptGroup from '@/views/scripts/components/script-group.vue'
defineProps({
hostId: {
required: true,
type: String
}
})
const emit = defineEmits(['exec-command',])
const { proxy: { $store, $message } } = getCurrentInstance()
const router = useRouter()
const scriptContent = ref('')
const activeGroup = ref('')
const scriptEditVisible = ref(false)
const editScriptData = ref(null)
const scriptGroupVisible = ref(false)
const isPlusActive = computed(() => $store.isPlusActive)
const scriptGroupList = computed(() => $store.scriptGroupList || [])
const scriptList = computed(() => $store.scriptList || [])
const gotoPlusPage = () => {
router.push('/setting?tabKey=plus')
}
const filteredScripts = computed(() => {
return scriptList.value.filter(script => script.group === activeGroup.value)
})
const handleSelectGroup = (group) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
activeGroup.value = group.id
}
const handleExecScript = (script) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
emit('exec-command', script.command)
}
const handleEditScript = (script) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
editScriptData.value = script
scriptEditVisible.value = true
}
const handleSendToEditor = (script) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
scriptContent.value = script.command
}
const handleSendToTerminal = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
if (!scriptContent.value) return
emit('exec-command', scriptContent.value)
scriptContent.value = ''
}
const handleAddScript = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
editScriptData.value = null
scriptEditVisible.value = true
}
const handleScriptEditSuccess = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
editScriptData.value = null
}
const handleShowGroupManage = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
scriptGroupVisible.value = true
}
const handleGroupDeleted = (deletedGroupId) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
if (deletedGroupId === activeGroup.value) {
// 如果删除的是当前选中的分组,切换到第一个分组
if (scriptGroupList.value.length) {
activeGroup.value = scriptGroupList.value[0].id
}
}
}
const handleSaveAsScript = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
if (!scriptContent.value) {
return $message.warning('请先输入脚本内容')
}
scriptEditVisible.value = true
}
if (scriptGroupList.value.length) {
activeGroup.value = scriptGroupList.value[0].id
}
</script>
<style lang="scss" scoped>
.script_input_container {
height: 100%;
display: flex;
gap: 10px;
padding: 10px;
min-width: 0;
position: relative;
.plus_tips {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
.plus_tips_content {
text-align: center;
color: #fff;
.lock-icon {
font-size: 32px;
margin-bottom: 12px;
}
.plus_label {
font-size: 20px;
font-weight: bold;
color: #ffd700;
margin-bottom: 8px;
}
p {
font-size: 14px;
margin: 0;
opacity: 0.9;
.to_active {
color: #ffd700;
text-decoration: underline;
cursor: pointer;
}
}
}
}
.left_box {
width: 50%;
min-width: 50%;
max-width: 50%;
flex: 0 0 50%;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 4px;
.group_list {
padding: 6px 10px;
height: 32px;
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
display: flex;
flex-wrap: nowrap;
gap: 8px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
&::-webkit-scrollbar {
display: none;
}
.group_item {
padding: 2px 12px;
cursor: pointer;
border-radius: 12px;
margin-bottom: 0;
font-size: 13px;
border: 1px solid var(--el-border-color-light);
flex-shrink: 0;
&:hover {
background-color: var(--el-fill-color-light);
border-color: var(--el-border-color);
}
&.active {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-color: var(--el-color-primary-light-7);
}
&.group_manage {
display: flex;
align-items: center;
gap: 4px;
background-color: var(--el-color-info-light-9);
border-color: var(--el-color-info-light-5);
color: var(--el-text-color-regular);
.el-icon {
font-size: 14px;
}
&:hover {
background-color: var(--el-color-info-light-7);
border-color: var(--el-color-info);
color: var(--el-color-info-dark-2);
}
}
}
}
.script_list {
flex: 1;
overflow-y: auto;
padding: 10px;
padding-bottom: 30px;
.empty-list {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
:deep(.el-empty__description) {
margin-bottom: 10px;
p {
margin: 0;
line-height: 1.5;
&.sub-text {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
.script_item {
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
padding: 0 12px;
border-radius: 4px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s ease;
line-height: 36px;
&:hover {
background-color: var(--el-fill-color-light);
border-color: var(--el-border-color);
.hover_icons {
opacity: 1;
transform: translateX(0);
}
}
.script_info {
flex: 1;
display: flex;
align-items: center;
min-width: 0;
margin-right: 8px;
height: 100%;
padding: 2px 0;
.script_name {
flex-shrink: 0;
font-size: 13px;
color: var(--el-text-color-regular);
margin-right: 4px;
line-height: normal;
&::before {
content: '⌘';
margin-right: 6px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
}
.script_command {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: var(--el-text-color-secondary);
line-height: normal;
padding: 2px 0;
}
}
.hover_icons {
display: flex;
gap: 8px;
opacity: 0;
transform: translateX(10px);
transition: all 0.2s ease;
flex-shrink: 0;
height: 100%;
align-items: center;
padding: 2px 0;
.action_icon {
font-size: 16px;
color: var(--el-text-color-secondary);
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
&:hover {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
transform: scale(1.1);
}
}
}
&.add_script_btn {
justify-content: center;
color: var(--el-text-color-secondary);
border: 1px dashed var(--el-border-color);
margin-top: 10px;
background-color: var(--el-fill-color-blank);
.el-icon {
font-size: 14px;
margin-right: 4px;
}
span {
font-size: 13px;
}
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
}
}
}
.right_box {
width: 50%;
min-width: 50%;
max-width: 50%;
flex: 0 0 50%;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 4px;
position: relative;
.editor_title {
padding: 6px 10px;
height: 32px;
box-sizing: border-box;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
border-bottom: 1px solid var(--el-border-color);
display: flex;
align-items: center;
}
.editor_content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
padding: 10px;
height: calc(100% - 32px);
.el-textarea {
height: 100%;
:deep(.el-textarea__inner) {
height: 100%;
resize: none;
}
}
.action_btn {
position: absolute;
right: 20px;
bottom: 20px;
z-index: 1;
display: flex;
gap: 10px;
}
}
}
}
</style>
+4 -55
View File
@@ -1,6 +1,5 @@
<template>
<div ref="sftpTabContainerRef" class="sftp_tab_container">
<div ref="adjustRef" class="adjust" />
<div class="sftp_tab_container">
<section>
<div class="left box">
<div class="header">
@@ -159,7 +158,7 @@
import { ref, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
import socketIo from 'socket.io-client'
import CodeEdit from '@/components/code-edit/index.vue'
import { EventBus, isDir, isFile, sortDirTree, downloadFile, isMobile } from '@/utils'
import { isDir, isFile, sortDirTree, downloadFile, isMobile } from '@/utils'
import dirIcon from '@/assets/image/system/dir.png'
import linkIcon from '@/assets/image/system/link.png'
import fileIcon from '@/assets/image/system/file.png'
@@ -174,8 +173,6 @@ const props = defineProps({
}
})
const emit = defineEmits(['resize',])
const { proxy: { $store, $notification, $message, $messageBox, $serviceURI, $nextTick } } = getCurrentInstance()
const visible = ref(false)
@@ -204,8 +201,6 @@ const curUploadFileName = ref('')
const showDirProgress = ref(false)
const curUploadDirName = ref(0)
const adjustRef = ref(null)
const sftpTabContainerRef = ref(null)
const childDirRef = ref(null)
const uploadFileRef = ref(null)
const uploadDirRef = ref(null)
@@ -221,46 +216,12 @@ const fileList = computed(() => childDir.value.filter(({ name }) => name.include
onMounted(() => {
connectSftp()
adjustHeight()
EventBus.$on('update-sftp-tab-height', () => {
adjustHeight()
})
})
onBeforeUnmount(() => {
if (socket.value) socket.value.close()
})
const adjustHeight = async () => {
let startAdjust = false
let timer = null
await $nextTick()
try {
let sftpHeight = localStorage.getItem('sftpHeight')
if (sftpHeight) sftpTabContainerRef.value.style.height = sftpHeight
adjustRef.value.addEventListener('mousedown', () => {
startAdjust = true
})
document.addEventListener('mousemove', (e) => {
if (!startAdjust) return
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
sftpHeight = `calc(100vh - ${ e.pageY }px - 20px)`
sftpTabContainerRef.value.style.height = sftpHeight
emit('resize')
})
})
document.addEventListener('mouseup', () => {
if (!startAdjust) return
startAdjust = false
localStorage.setItem('sftpHeight', sftpHeight)
EventBus.$emit('update-sftp-tab-height')
})
} catch (error) {
console.warn(error.message)
}
}
const connectSftp = () => {
socket.value = io($serviceURI, {
path: '/sftp',
@@ -653,23 +614,10 @@ defineExpose({
<style lang="scss" scoped>
.sftp_tab_container {
position: relative;
background: #ffffff;
border: 1px solid var(--el-border-color);
.adjust {
user-select: none;
position: absolute;
top: -3px;
width: 100%;
height: 5px;
background: var(--el-color-primary);
opacity: 0.3;
cursor: ns-resize;
}
height: 100%;
section {
height: 100%;
display: flex;
// common
.box {
$header_height: 30px;
.header {
@@ -726,6 +674,7 @@ defineExpose({
user-select: none;
display: flex;
flex-direction: column;
padding-bottom: 30px;
.active {
background: #e9e9e9;
}
@@ -444,8 +444,8 @@ const focusTab = () => {
}, 200)
}
const inputCommand = (command) => {
command = command + (autoExecuteScript.value ? '\n' : '')
const inputCommand = (command, isSyncAllSession = false) => {
command = command + (isSyncAllSession ? '' : (autoExecuteScript.value ? '\n' : ''))
socket.value.emit('input', command)
}
+37 -42
View File
@@ -16,6 +16,7 @@
</template>
</el-dropdown>
<el-dropdown
v-if="!isPlusActive"
trigger="click"
max-height="50vh"
:teleported="false"
@@ -50,9 +51,9 @@
<span class="link_text">功能项<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="showInputCommand = true">
<!-- <el-dropdown-item @click="showInputCommand = true">
<span>长指令输入</span>
</el-dropdown-item>
</el-dropdown-item> -->
<el-dropdown-item @click="handleFullScreen">
<span>启用全屏</span>
</el-dropdown-item>
@@ -72,7 +73,7 @@
<div class="switch_wrap">
<el-tooltip
effect="dark"
content="开启后同步键盘输入到所有会话"
content="同步键盘输入到所有会话"
placement="bottom"
>
<el-switch
@@ -88,16 +89,16 @@
<div class="switch_wrap">
<el-tooltip
effect="dark"
content="SFTP文件传输"
content="包含SFTP文件传输、脚本库等功能"
placement="bottom"
>
<el-switch
v-model="showSftp"
v-model="showFooterBar"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="SFTP"
inactive-text="SFTP"
active-text="工具栏"
inactive-text="工具栏"
/>
</el-tooltip>
</div>
@@ -148,13 +149,13 @@
</div>
</template>
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
<!-- @cd-command="cdCommand" -->
<TerminalTab
ref="terminalRefs"
:host-obj="item"
:long-press-ctrl="longPressCtrl"
:long-press-alt="longPressAlt"
@input-command="terminalInput"
@cd-command="cdCommand"
@ping-data="getPingData"
@reset-long-press="resetLongPress"
/>
@@ -164,11 +165,11 @@
:long-press-alt="longPressAlt"
@click-key="handleClickVirtualKeyboard"
/>
<Sftp
v-if="showSftp"
ref="sftpRefs"
<FooterBar
v-if="showFooterBar"
:host-id="item.id"
@resize="resizeTerminal"
@exec-command="handleInputCommand"
/>
</div>
</el-tab-pane>
@@ -197,9 +198,9 @@ import FloatMenu from '@/components/float-menu/index.vue'
import { terminalStatusList, virtualKeyType } from '@/utils/enum'
import TerminalTab from './terminal-tab.vue'
import InfoSide from './info-side.vue'
import Sftp from './sftp.vue'
import HostForm from '../../server/components/host-form.vue'
import TerminalSetting from './terminal-setting.vue'
import FooterBar from './footer-bar.vue'
const { proxy: { $nextTick, $store, $message } } = getCurrentInstance()
@@ -216,12 +217,12 @@ const showInputCommand = ref(false)
const infoSideRef = ref(null)
const pingData = ref({})
const terminalRefs = ref([])
const sftpRefs = ref([])
// const sftpRefs = ref([])
const activeTabIndex = ref(0)
const visible = ref(true)
const showSftp = ref(localStorage.getItem('showSftp') === 'true')
const mainHeight = ref('')
const showFooterBar = ref(localStorage.getItem('showFooterBar') === 'true')
const isSyncAllSession = ref(false)
const mainHeight = ref('')
const hostFormVisible = ref(false)
const updateHostData = ref(null)
const showSetting = ref(false)
@@ -229,6 +230,7 @@ const showMobileInfoSideDialog = ref(false)
const longPressCtrl = ref(false)
const longPressAlt = ref(false)
const isPlusActive = computed(() => $store.isPlusActive)
const terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length)
const hostList = computed(() => $store.hostList)
@@ -328,21 +330,22 @@ const terminalInput = (command) => {
return index !== activeTabIndex.value
})
filterTerminalRefs.forEach(hostRef => {
hostRef.inputCommand(command)
hostRef.inputCommand(command, true)
})
}
const cdCommand = (path) => {
// console.log('cdCommand:', path)
if (!showSftp.value) return
if (isSyncAllSession.value) {
sftpRefs.value.forEach(sftpRef => {
sftpRef.openDir(path)
})
} else {
sftpRefs.value[activeTabIndex.value].openDir(path, false)
}
}
// 识别命令动态切换目录功能暂时取消
// const cdCommand = (path) => {
// // console.log('cdCommand:', path)
// if (!showSftp.value) return
// if (isSyncAllSession.value) {
// sftpRefs.value.forEach(sftpRef => {
// sftpRef.openDir(path)
// })
// } else {
// sftpRefs.value[activeTabIndex.value].openDir(path, false)
// }
// }
const getPingData = (data) => {
pingData.value[data.ip] = data
@@ -367,19 +370,15 @@ watch(terminalTabsLen, () => {
deep: false
})
watch(showSftp, () => {
localStorage.setItem('showSftp', showSftp.value)
nextTick(() => {
resizeTerminal()
})
watch(showFooterBar, async () => {
localStorage.setItem('showFooterBar', showFooterBar.value)
await $nextTick()
resizeTerminal()
}, {
immediate: true,
deep: false
})
// const windowBeforeUnload = () => {
// window.onbeforeunload = () => {
// return ''
// }
// }
const removeTab = (index) => {
emit('removeTab', index)
if (index === activeTabIndex.value) {
@@ -542,10 +541,6 @@ const handleInputCommand = async (command) => {
:deep(.terminal_tab_container) {
flex: 1;
}
:deep(.sftp_tab_container) {
height: 300px;
}
}
.full-screen-button {