:feat: 新增终端链接跳转需按住ctrl键(防止误触)&AI对话组件优化

This commit is contained in:
chaos-zhu
2025-04-17 22:20:04 +08:00
parent dfee5249cf
commit 2bf98bc886
4 changed files with 153 additions and 159 deletions

View File

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

View File

@@ -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('清除成功')
}

View File

@@ -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: 重写终端搜索功能

View File

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