mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-07 16:48:04 +08:00
✨ 增强脚本输入功能&优化与修复若干项
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.4",
|
||||
"description": "easynode-web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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' })
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -97,6 +97,7 @@ const plusFeatures = [
|
||||
'服务器跳板机功能,支持任意数量服务器的连续跳板',
|
||||
'批量修改实例配置(优化版)',
|
||||
'脚本库批量导出导入',
|
||||
'脚本库分组与终端工具栏支持',
|
||||
'凭据管理支持解密带密码保护的密钥',
|
||||
'通知方式无限制',
|
||||
'本地socket断开自动重连',
|
||||
|
||||
+113
-16
@@ -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 {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user