mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-05-20 14:57:36 +08:00
:feat: 新增终端链接跳转需按住ctrl键(防止误触)&AI对话组件优化
This commit is contained in:
@@ -51,11 +51,9 @@ async function saveChatHistory({ res, request }) {
|
||||
if (!chatList) return res.fail({ data: false, msg: '参数错误' })
|
||||
let updateChat = chatRecord
|
||||
if (id) {
|
||||
console.log('存在-更新')
|
||||
chatRecord.updatedAt = Date.now()
|
||||
await chatHistoryDB.updateAsync({ _id: id }, chatRecord)
|
||||
} else {
|
||||
console.log('不存在-创建')
|
||||
chatRecord.createdAt = Date.now()
|
||||
delete chatRecord.id
|
||||
const result = await chatHistoryDB.insertAsync(chatRecord)
|
||||
|
||||
@@ -158,11 +158,11 @@ export function useAIChat() {
|
||||
}
|
||||
}
|
||||
|
||||
const clearChat = () => {
|
||||
const clearChat = async () => {
|
||||
if (loading.value) return
|
||||
chatList.value.length = 1
|
||||
error.value = null
|
||||
saveChat() // 保存清空后的状态
|
||||
await saveChat() // 保存清空后的状态
|
||||
ElMessage.success('清除成功')
|
||||
}
|
||||
|
||||
|
||||
@@ -290,7 +290,9 @@ const handleResize = () => {
|
||||
}
|
||||
|
||||
const onWebLinks = () => {
|
||||
term.value.loadAddon(new WebLinksAddon())
|
||||
term.value.loadAddon(new WebLinksAddon((event, uri) => {
|
||||
if (event.ctrlKey || event.altKey) window.open(uri, '_blank')
|
||||
}))
|
||||
}
|
||||
|
||||
// :TODO: 重写终端搜索功能
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
<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
|
||||
@@ -31,9 +29,7 @@
|
||||
: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-cascader-panel
|
||||
v-if="scriptLibraryCascader"
|
||||
@@ -73,9 +69,7 @@
|
||||
</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">
|
||||
@@ -240,193 +234,193 @@ import {
|
||||
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";
|
||||
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();
|
||||
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 scriptIdsPath = 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 scriptIdsPath = ref([])
|
||||
|
||||
const isPlusActive = computed(() => $store.isPlusActive);
|
||||
const terminalTabs = computed(() => props.terminalTabs);
|
||||
const terminalTabsLen = computed(() => props.terminalTabs.length);
|
||||
const hostList = computed(() => $store.hostList);
|
||||
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 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";
|
||||
const groupId = script.group || 'default'
|
||||
if (!acc[groupId]) {
|
||||
acc[groupId] = [];
|
||||
acc[groupId] = []
|
||||
}
|
||||
acc[groupId].push({
|
||||
value: script.id,
|
||||
label: script.name,
|
||||
command: script.command, // 保存command用于执行脚本
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
command: script.command // 保存command用于执行脚本
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// 将分组转换为级联面板所需的格式
|
||||
return scriptGroupList.value.map((group) => ({
|
||||
value: group.id,
|
||||
label: group.name,
|
||||
children: scriptsByGroup[group.id] || [],
|
||||
}));
|
||||
});
|
||||
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"
|
||||
);
|
||||
};
|
||||
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
|
||||
});
|
||||
};
|
||||
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 = ({ id: scriptId }) => {
|
||||
const id = scriptLibraryCascader.value
|
||||
? scriptIdsPath.value.pop()
|
||||
: scriptId;
|
||||
const script = scriptList.value.find((item) => item.id === id);
|
||||
if (!script) return $message.warning("未找到对应的脚本");
|
||||
: 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);
|
||||
const command = script.command
|
||||
if (!isSyncAllSession.value) return handleInputCommand(command)
|
||||
terminalRefs.value.forEach((terminalRef) => {
|
||||
terminalRef.inputCommand(command);
|
||||
});
|
||||
};
|
||||
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;
|
||||
});
|
||||
return index !== activeTabIndex.value
|
||||
})
|
||||
filterTerminalRefs.forEach((hostRef) => {
|
||||
hostRef.inputCommand(command, true);
|
||||
});
|
||||
};
|
||||
hostRef.inputCommand(command, true)
|
||||
})
|
||||
}
|
||||
|
||||
// 识别命令动态切换目录功能暂时取消
|
||||
// const cdCommand = (path) => {
|
||||
@@ -442,59 +436,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;
|
||||
let len = terminalTabsLen.value
|
||||
// console.log('add tab:', len)
|
||||
if (len > 0) {
|
||||
activeTabIndex.value = len - 1;
|
||||
activeTabIndex.value = len - 1
|
||||
// registryDbClick()
|
||||
tabChange(activeTabIndex.value);
|
||||
tabChange(activeTabIndex.value)
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: false,
|
||||
deep: false
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
watch(
|
||||
showFooterBar,
|
||||
async () => {
|
||||
localStorage.setItem("showFooterBar", showFooterBar.value);
|
||||
await $nextTick();
|
||||
resizeTerminal();
|
||||
localStorage.setItem('showFooterBar', showFooterBar.value)
|
||||
await $nextTick()
|
||||
resizeTerminal()
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: false,
|
||||
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();
|
||||
};
|
||||
.getElementsByClassName('terminal_and_sftp_wrap')[0]
|
||||
.requestFullscreen()
|
||||
}
|
||||
|
||||
// const registryDbClick = () => {
|
||||
// $nextTick(() => {
|
||||
@@ -513,18 +507,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>
|
||||
|
||||
Reference in New Issue
Block a user