✨ 支持脚本库菜单自定义展示

This commit is contained in:
chaos-zhu
2025-03-30 22:26:21 +08:00
parent d43e1a3505
commit af08dd9e1e
6 changed files with 320 additions and 158 deletions
+5 -3
View File
@@ -1,10 +1,12 @@
## [3.0.5](https://github.com/chaos-zhu/easynode/releases) (2025-03-18)
## [3.0.5](https://github.com/chaos-zhu/easynode/releases) (2025-04-xx)
* 新增AI问答&集成脚本生成功能(TODO
* 新增服务器定时任务功能(TODO
* 连接服务器下拉菜单分组展示(TODO
* 增加升级更新内容展示(TODO
* 面板支持灵活配置IP白名单
* serv00 SFTP修复(TODO
* 脚本库菜单自定义展示(可在本地设置中配置)
* 字体优化
* 支持在面板配置IP白名单
## [3.0.4](https://github.com/chaos-zhu/easynode/releases) (2025-03-15)
-3
View File
@@ -5,7 +5,6 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0">
<!-- <meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes"> -->
<title>EasyNode</title>
<script src="//at.alicdn.com/t/c/font_3309550_x7zmcgwaxf.js"></script>
</head>
@@ -13,8 +12,6 @@
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<!-- <script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script> -->
</body>
</html>
+5
View File
@@ -1,5 +1,10 @@
:root {
--el-font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
html {
font-size: 15px;
font-family: var(--el-font-family);
}
html,
+12
View File
@@ -45,6 +45,13 @@ const useStore = defineStore('global', {
},
...(localStorage.getItem('terminalConfig') ? JSON.parse(localStorage.getItem('terminalConfig')) : {})
},
menuSetting: {
...{
scriptLibrary: false,
scriptLibraryCascader: true
},
...(localStorage.getItem('menuSetting') ? JSON.parse(localStorage.getItem('menuSetting')) : {})
},
plusInfo: {},
isPlusActive: false
}),
@@ -127,6 +134,11 @@ const useStore = defineStore('global', {
localStorage.setItem('terminalConfig', JSON.stringify(newConfig))
this.$patch({ terminalConfig: newConfig })
},
setMenuSetting(setTarget = {}) {
let newConfig = { ...this.menuSetting, ...setTarget }
localStorage.setItem('menuSetting', JSON.stringify(newConfig))
this.$patch({ menuSetting: newConfig })
},
async wsClientsStatus() {
// if (this.HostStatusSocket) this.HostStatusSocket.close()
let socketInstance = io(this.serviceURI, {
@@ -132,6 +132,31 @@
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="菜单选项">
<el-form
ref="formRef"
label-suffix=":"
label-width="100px"
:show-message="false"
>
<el-form-item label="脚本库" prop="scriptLibrary">
<span class="script_library_switch">
<el-switch
v-model="scriptLibrary"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="开启"
inactive-text="关闭"
/>
</span>
<el-radio-group v-model="scriptLibraryCascader" size="small" text-color="#fff" fill="#13ce66">
<el-radio-button :value="true">分组级联展示</el-radio-button>
<el-radio-button :value="false">不分组单列展示</el-radio-button>
</el-radio-group>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<template #footer>
<span class="dialog_footer">
@@ -191,6 +216,14 @@ const autoExecuteScript = computed({
get: () => $store.terminalConfig.autoExecuteScript,
set: (newVal) => $store.setTerminalSetting({ autoExecuteScript: newVal })
})
const scriptLibrary = computed({
get: () => $store.menuSetting.scriptLibrary,
set: (newVal) => $store.setMenuSetting({ scriptLibrary: newVal })
})
const scriptLibraryCascader = computed({
get: () => $store.menuSetting.scriptLibraryCascader,
set: (newVal) => $store.setMenuSetting({ scriptLibraryCascader: newVal })
})
const isPlusActive = computed(() => $store.isPlusActive)
const changeBackground = (item) => {
@@ -239,6 +272,9 @@ const changeBackground = (item) => {
display: flex;
justify-content: center;
}
.script_library_switch {
margin-right: 20px;
}
</style>
<style lang="scss">
+262 -152
View File
@@ -3,29 +3,54 @@
<div class="terminal_top">
<div class="left_menu">
<el-dropdown trigger="click">
<span class="link_text">连接<el-icon><arrow-down /></el-icon></span>
<span class="link_text"
>连接<el-icon><arrow-down /></el-icon
></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item class="link_close_all" @click="handleCloseAllTab">
<el-dropdown-item
class="link_close_all"
@click="handleCloseAllTab"
>
<span>关闭所有连接</span>
</el-dropdown-item>
<el-dropdown-item v-for="(item, index) in hostList" :key="index" @click="handleLinkHost(item)">
<el-dropdown-item
v-for="(item, index) in hostList"
:key="index"
@click="handleLinkHost(item)"
>
{{ item.name }} {{ item.host }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown
v-if="!isPlusActive"
v-if="scriptLibrary"
trigger="click"
max-height="50vh"
:teleported="false"
class="scripts_menu"
>
<span class="link_text">脚本库<el-icon><arrow-down /></el-icon></span>
<span class="link_text"
>脚本库<el-icon><arrow-down /></el-icon
></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in scriptList" :key="item.id" @click="handleExecScript(item)">
<el-cascader-panel
v-if="scriptLibraryCascader"
v-model="scriptIdsPath"
style="width: fit-content"
:props="{
expandTrigger: 'hover',
}"
:options="formatScriptList"
@change="handleExecScript"
/>
<el-dropdown-menu v-else>
<el-dropdown-item
v-for="item in scriptList"
:key="item.id"
@click="handleExecScript(item)"
>
<span>{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
@@ -48,7 +73,9 @@
</template>
</el-dropdown> -->
<el-dropdown trigger="click">
<span class="link_text">功能项<el-icon><arrow-down /></el-icon></span>
<span class="link_text"
>功能项<el-icon><arrow-down /></el-icon
></span>
<template #dropdown>
<el-dropdown-menu>
<!-- <el-dropdown-item @click="showInputCommand = true">
@@ -66,7 +93,11 @@
</div>
<div class="right_overview">
<div v-if="isMobileScreen" class="switch_wrap">
<el-button :type="curHost?.monitorData?.connect ? 'success' : 'danger'" text @click="() => showMobileInfoSideDialog = true">
<el-button
:type="curHost?.monitorData?.connect ? 'success' : 'danger'"
text
@click="() => (showMobileInfoSideDialog = true)"
>
状态
</el-button>
</div>
@@ -80,7 +111,10 @@
v-model="isSyncAllSession"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
style="
--el-switch-on-color: #13ce66;
--el-switch-off-color: #ff4949;
"
active-text="同步"
inactive-text="同步"
/>
@@ -96,7 +130,10 @@
v-model="showFooterBar"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
style="
--el-switch-on-color: #13ce66;
--el-switch-off-color: #ff4949;
"
active-text="工具栏"
inactive-text="工具栏"
/>
@@ -144,7 +181,10 @@
>
<template #label>
<div class="tab_label">
<span class="tab_status" :style="{ background: getStatusColor(item.status) }" />
<span
class="tab_status"
:style="{ background: getStatusColor(item.status) }"
/>
<span>{{ item.name }}</span>
</div>
</template>
@@ -176,7 +216,10 @@
</el-tabs>
</div>
<InputCommand v-model:show="showInputCommand" @input-command="handleInputCommand" />
<InputCommand
v-model:show="showInputCommand"
@input-command="handleInputCommand"
/>
<HostForm
v-model:show="hostFormVisible"
@@ -190,149 +233,200 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue'
import useMobileWidth from '@/composables/useMobileWidth'
import InputCommand from '@/components/input-command/index.vue'
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 HostForm from '../../server/components/host-form.vue'
import TerminalSetting from './terminal-setting.vue'
import FooterBar from './footer-bar.vue'
import {
ref,
computed,
getCurrentInstance,
watch,
onMounted,
onBeforeUnmount,
nextTick,
} from "vue";
import { ArrowDown } from "@element-plus/icons-vue";
import useMobileWidth from "@/composables/useMobileWidth";
import InputCommand from "@/components/input-command/index.vue";
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 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()
const {
proxy: { $nextTick, $store, $message },
} = getCurrentInstance();
const props = defineProps({
terminalTabs: {
type: Array,
required: true
}
})
required: true,
},
});
const emit = defineEmits(['closed', 'close-all-tab', 'removeTab', 'add-host',])
const { isMobileScreen } = useMobileWidth()
const showInputCommand = ref(false)
const infoSideRef = ref(null)
const pingData = ref({})
const terminalRefs = ref([])
const emit = defineEmits(["closed", "close-all-tab", "removeTab", "add-host"]);
const { isMobileScreen } = useMobileWidth();
const showInputCommand = ref(false);
const infoSideRef = ref(null);
const pingData = ref({});
const terminalRefs = ref([]);
// const sftpRefs = ref([])
const activeTabIndex = ref(0)
const visible = ref(true)
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)
const showMobileInfoSideDialog = ref(false)
const longPressCtrl = ref(false)
const longPressAlt = ref(false)
const activeTabIndex = ref(0);
const visible = ref(true);
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);
const showMobileInfoSideDialog = ref(false);
const longPressCtrl = ref(false);
const longPressAlt = ref(false);
const scriptIdsPath = ref([]);
const isPlusActive = computed(() => $store.isPlusActive)
const terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length)
const hostList = computed(() => $store.hostList)
const curHost = computed(() => hostList.value.find(item => item.host === terminalTabs.value[activeTabIndex.value]?.host))
const scriptList = computed(() => $store.scriptList)
const isPlusActive = computed(() => $store.isPlusActive);
const terminalTabs = computed(() => props.terminalTabs);
const terminalTabsLen = computed(() => props.terminalTabs.length);
const hostList = computed(() => $store.hostList);
const curHost = computed(() =>
hostList.value.find(
(item) => item.host === terminalTabs.value[activeTabIndex.value]?.host
)
);
const scriptGroupList = computed(() => $store.scriptGroupList);
const scriptList = computed(() => $store.scriptList);
const scriptLibrary = computed(() => $store.menuSetting.scriptLibrary);
const scriptLibraryCascader = computed(
() => $store.menuSetting.scriptLibraryCascader
);
const formatScriptList = computed(() => {
// 首先创建一个以分组id为key的脚本映射
const scriptsByGroup = scriptList.value.reduce((acc, script) => {
const groupId = script.group || "default";
if (!acc[groupId]) {
acc[groupId] = [];
}
acc[groupId].push({
value: script.id,
label: script.name,
command: script.command, // 保存command用于执行脚本
});
return acc;
}, {});
// 将分组转换为级联面板所需的格式
return scriptGroupList.value.map((group) => ({
value: group.id,
label: group.name,
children: scriptsByGroup[group.id] || [],
}));
});
onMounted(() => {
handleResizeTerminalSftp()
window.addEventListener('resize', handleResizeTerminalSftp)
})
handleResizeTerminalSftp();
window.addEventListener("resize", handleResizeTerminalSftp);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResizeTerminalSftp)
})
window.removeEventListener("resize", handleResizeTerminalSftp);
});
const getStatusColor = (status) => {
return terminalStatusList.find(item => item.value === status)?.color || 'gray'
}
return (
terminalStatusList.find((item) => item.value === status)?.color || "gray"
);
};
const handleUpdateList = async ({ host }) => {
try {
await $store.getHostList()
let targetHost = hostList.value.find(item => item.host === host)
if (targetHost) emit('add-host', targetHost)
await $store.getHostList();
let targetHost = hostList.value.find((item) => item.host === host);
if (targetHost) emit("add-host", targetHost);
} catch (err) {
$message.error('获取实例列表失败')
console.error('获取实例列表失败: ', err)
$message.error("获取实例列表失败");
console.error("获取实例列表失败: ", err);
}
}
};
const handleResizeTerminalSftp = () => {
$nextTick(() => {
mainHeight.value = document.querySelector('.terminal_and_sftp_wrap')?.offsetHeight - 45 // 45 is tab-header height+15
})
}
mainHeight.value =
document.querySelector(".terminal_and_sftp_wrap")?.offsetHeight - 45; // 45 is tab-header height+15
});
};
const handleLinkHost = (host) => {
if (!host.isConfig) {
$message.warning('请先配置SSH连接信息')
hostFormVisible.value = true
updateHostData.value = { ...host }
return
$message.warning("请先配置SSH连接信息");
hostFormVisible.value = true;
updateHostData.value = { ...host };
return;
}
emit('add-host', host)
}
emit("add-host", host);
};
const handleCloseAllTab = () => {
emit('close-all-tab')
}
emit("close-all-tab");
};
const { LONG_PRESS, SINGLE_PRESS } = virtualKeyType
const { LONG_PRESS, SINGLE_PRESS } = virtualKeyType;
const handleClickVirtualKeyboard = async (virtualKey) => {
const { key, ansi ,type } = virtualKey
const { key, ansi, type } = virtualKey;
// console.log(key, ascii, ansi, type)
switch (type) {
case LONG_PRESS:
// console.log('待组合键')
if (key === 'Ctrl') {
longPressCtrl.value = true
longPressAlt.value = false
if (key === "Ctrl") {
longPressCtrl.value = true;
longPressAlt.value = false;
}
if (key === 'Alt') {
longPressAlt.value = true
longPressCtrl.value = false
if (key === "Alt") {
longPressAlt.value = true;
longPressCtrl.value = false;
}
// eslint-disable-next-line no-case-declarations
const curTerminalRef = terminalRefs.value[activeTabIndex.value]
await $nextTick()
curTerminalRef?.focusTab()
break
const curTerminalRef = terminalRefs.value[activeTabIndex.value];
await $nextTick();
curTerminalRef?.focusTab();
break;
case SINGLE_PRESS:
longPressCtrl.value = false
longPressAlt.value = false
handleExecScript({ command: ansi })
break
longPressCtrl.value = false;
longPressAlt.value = false;
handleExecScript({ command: ansi });
break;
default:
break
break;
}
}
};
const resetLongPress = () => {
longPressCtrl.value = false
longPressAlt.value = false
}
longPressCtrl.value = false;
longPressAlt.value = false;
};
const handleExecScript = (scriptObj) => {
let { command } = scriptObj
if (!isSyncAllSession.value) return handleInputCommand(command)
terminalRefs.value.forEach(terminalRef => {
terminalRef.inputCommand(command)
})
}
const handleExecScript = ({ id: scriptId }) => {
const id = scriptLibraryCascader.value
? scriptIdsPath.value.pop()
: scriptId;
const script = scriptList.value.find((item) => item.id === id);
if (!script) return $message.warning("未找到对应的脚本");
const command = script.command;
if (!isSyncAllSession.value) return handleInputCommand(command);
terminalRefs.value.forEach((terminalRef) => {
terminalRef.inputCommand(command);
});
};
const terminalInput = (command) => {
if (!isSyncAllSession.value) return
if (!isSyncAllSession.value) return;
let filterTerminalRefs = terminalRefs.value.filter((host, index) => {
return index !== activeTabIndex.value
})
filterTerminalRefs.forEach(hostRef => {
hostRef.inputCommand(command, true)
})
}
return index !== activeTabIndex.value;
});
filterTerminalRefs.forEach((hostRef) => {
hostRef.inputCommand(command, true);
});
};
// 识别命令动态切换目录功能暂时取消
// const cdCommand = (path) => {
@@ -348,49 +442,59 @@ const terminalInput = (command) => {
// }
const getPingData = (data) => {
pingData.value[data.ip] = data
}
pingData.value[data.ip] = data;
};
const tabChange = async (index) => {
await $nextTick()
const curTerminalRef = terminalRefs.value[index]
curTerminalRef?.focusTab()
}
await $nextTick();
const curTerminalRef = terminalRefs.value[index];
curTerminalRef?.focusTab();
};
watch(terminalTabsLen, () => {
let len = terminalTabsLen.value
// console.log('add tab:', len)
if (len > 0) {
activeTabIndex.value = len - 1
// registryDbClick()
tabChange(activeTabIndex.value)
watch(
terminalTabsLen,
() => {
let len = terminalTabsLen.value;
// console.log('add tab:', len)
if (len > 0) {
activeTabIndex.value = len - 1;
// registryDbClick()
tabChange(activeTabIndex.value);
}
},
{
immediate: true,
deep: false,
}
}, {
immediate: true,
deep: false
})
);
watch(showFooterBar, async () => {
localStorage.setItem('showFooterBar', showFooterBar.value)
await $nextTick()
resizeTerminal()
}, {
immediate: true,
deep: false
})
watch(
showFooterBar,
async () => {
localStorage.setItem("showFooterBar", showFooterBar.value);
await $nextTick();
resizeTerminal();
},
{
immediate: true,
deep: false,
}
);
const removeTab = (index) => {
emit('removeTab', index)
emit("removeTab", index);
if (index === activeTabIndex.value) {
nextTick(() => {
activeTabIndex.value = 0
})
activeTabIndex.value = 0;
});
}
}
};
const handleFullScreen = () => {
document.getElementsByClassName('terminal_and_sftp_wrap')[0].requestFullscreen()
}
document
.getElementsByClassName("terminal_and_sftp_wrap")[0]
.requestFullscreen();
};
// const registryDbClick = () => {
// $nextTick(() => {
@@ -409,18 +513,18 @@ const handleFullScreen = () => {
const resizeTerminal = () => {
for (let terminalTabRef of terminalRefs.value) {
const { handleResize } = terminalTabRef || {}
handleResize && handleResize()
const { handleResize } = terminalTabRef || {};
handleResize && handleResize();
}
}
};
const handleInputCommand = async (command) => {
const curTerminalRef = terminalRefs.value[activeTabIndex.value]
await $nextTick()
curTerminalRef?.focusTab()
curTerminalRef.inputCommand(`${ command }`)
showInputCommand.value = false
}
const curTerminalRef = terminalRefs.value[activeTabIndex.value];
await $nextTick();
curTerminalRef?.focusTab();
curTerminalRef.inputCommand(`${command}`);
showInputCommand.value = false;
};
</script>
<style lang="scss" scoped>
@@ -489,11 +593,13 @@ const handleInputCommand = async (command) => {
.right_overview {
display: flex;
align-items: center;
.switch_wrap {
display: flex;
align-items: center;
margin-right: 5px;
}
.full_icon {
cursor: pointer;
@@ -519,10 +625,12 @@ const handleInputCommand = async (command) => {
display: flex;
flex-direction: column;
position: relative;
.tab_label {
display: flex;
align-items: center;
justify-content: center;
.tab_status {
display: inline-block;
width: 8px;
@@ -533,6 +641,7 @@ const handleInputCommand = async (command) => {
// background-color: var(--el-color-primary);
}
}
.tab_content_wrap {
display: flex;
flex-direction: column;
@@ -570,7 +679,8 @@ const handleInputCommand = async (command) => {
.action_icon {
color: var(--el-color-primary);
}
.link_close_all:hover {
color: #ff4949!important;
color: #ff4949 !important;
}
</style>
</style>