feat: 右键添加同步目录到SFTP

This commit is contained in:
chaos-zhu
2025-10-29 22:54:37 +08:00
parent 21f2cd40da
commit 19814b71e4
3 changed files with 103 additions and 76 deletions
@@ -1922,7 +1922,7 @@ const onRowContextMenu = (row, _column, event) => {
// 发送cd指令到终端
if (row.type === 'd' && props.showCdCommand) {
const cdCommand = `cd ${ currentPath.value }/${ row.name }`.replace(/\/+/g, '/')
const cdCommand = `cd ${ currentPath.value }/${ row.name }\n`.replace(/\/+/g, '/')
items.push({
label: '发送cd指令到终端',
onClick: () => {
@@ -2514,7 +2514,8 @@ defineExpose({
selectedRows: computed(() => selectedRows.value),
connectionStatus: computed(() => connectionStatus.value),
refresh,
openDir
openDir,
switchToPath
})
</script>
@@ -330,17 +330,18 @@
:class="getSplitItemClass(item.key, panelIndex)"
@click="setActiveSplit(item.key, panelIndex)"
>
<!-- @cd-command="cdCommand" -->
<Terminal
ref="terminalRefs"
:host-obj="item"
:long-press-ctrl="longPressCtrl"
:long-press-alt="longPressAlt"
:auto-focus="panelIndex === 1"
:show-sftp-side="showSftpSide"
@input-command="(cmd, uid) => terminalInput(cmd, uid)"
@ping-data="getPingData"
@reset-long-press="resetLongPress"
@tab-focus="handleTabFocus"
@sync-path-to-sftp="(path) => handleSyncPathToSftp(path)"
/>
</div>
</template>
@@ -354,6 +355,7 @@
class="mobile_menu_drawer"
>
<SftpV2
ref="sftpRefs"
:init-connect="showSftpSide"
:host-id="item.id"
@exec-script="handleExecScript"
@@ -373,6 +375,7 @@
<div class="sftp_resize_handle_line" />
</div>
<SftpV2
ref="sftpRefs"
:init-connect="showSftpSide"
:host-id="item.id"
@exec-script="handleExecScript"
@@ -491,6 +494,7 @@ const hostGroupCascaderRef = ref(null)
const hostDropdownRef = ref(null)
const singleWindowRef = ref(null)
const showDockerDialog = ref(false)
const sftpRefs = ref([])
// 当前聚焦终端 uid
const focusedUid = ref(null)
@@ -857,23 +861,18 @@ const terminalInput = (command, uid) => {
})
}
// 识别命令动态切换目录功能暂时取消
// const cdCommand = (path) => {
// // console.log('cdCommand:', path)
// if (!showSftpSide.value) return
// if (isSyncAllSession.value) {
// sftpRefs.value.forEach(sftpRef => {
// sftpRef.openDir(path)
// })
// } else {
// sftpRefs.value[activeTabIndex.value].openDir(path, false)
// }
// }
const getPingData = ({ host, time }) => {
pingData.value[host] = time
}
const handleSyncPathToSftp = (path) => {
// 获取当前标签页的SFTP组件引用
const sftpRef = sftpRefs.value[activeTabIndex.value]
if (sftpRef && sftpRef.switchToPath && showSftpSide.value) {
sftpRef.switchToPath(path, false)
}
}
const tabChange = async (index) => {
await $nextTick()
getFirstTerminalRefOfTab(index)?.focusTab()
+87 -60
View File
@@ -64,15 +64,18 @@ const props = defineProps({
type: Boolean,
default: false
},
autoFocus: { type: Boolean, default: true }
autoFocus: { type: Boolean, default: true },
showSftpSide: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['inputCommand', 'cdCommand', 'ping-data', 'reset-long-press', 'tab-focus',])
const emit = defineEmits(['inputCommand', 'ping-data', 'reset-long-press', 'tab-focus', 'sync-path-to-sftp',])
const socket = ref(null)
// const commandHistoryList = ref([])
const term = ref(null)
const terminalText = ref('')
const highlighter = ref(null)
const initCommand = ref('')
const timer = ref(null)
@@ -86,6 +89,9 @@ const curStatus = ref(CONNECTING)
const terminalRef = ref(null)
const { showMenu, closeMenu, isVisible } = useContextMenu()
// 临时路径同步回调
const tempPathSyncCallback = ref(null)
const theme = computed(() => themeList[$store.terminalConfig.themeName])
const fontSize = computed(() => $store.terminalConfig.fontSize)
const fontFamily = computed(() => $store.terminalConfig.fontFamily)
@@ -235,6 +241,11 @@ const connectIO = () => {
socket.value.emit('ws_terminal', { hostId: hostId.value })
socket.value.on('terminal_connect_success', () => {
socket.value.on('output', (str) => {
// 如果有临时路径同步回调,先调用它
if (tempPathSyncCallback.value) {
tempPathSyncCallback.value(str)
}
if (props.isSingleWindow && !isPlusActive.value) return
// 使用高亮器处理
@@ -244,8 +255,6 @@ const connectIO = () => {
} else {
term.value.write(str)
}
terminalText.value += str
})
socket.value.on('terminal_connect_shell_success', () => {
curStatus.value = CONNECT_SUCCESS
@@ -457,39 +466,6 @@ const handleSearchClose = () => {
const enterTimer = ref(null)
function filterAnsiSequences(str) {
// 使用正则表达式移除ANSI转义序列
// return str.replace(/\x1b\[[0-9;]*m|\x1b\[?[\d;]*[A-HJKSTfmin]/g, '')
// eslint-disable-next-line
return str.replace(/\x1b\[[0-9;]*[mGK]|(\x1b\][0-?]*[0-7;]*\x07)|(\x1b[\[\]()#%;][0-9;?]*[0-9A-PRZcf-ntqry=><])/g, '')
}
// 处理 Backspace,删除前一个字符
function applyBackspace(text) {
let result = []
for (let i = 0; i < text.length; i++) {
if (text[i] === '\b') {
if (result.length > 0) {
result.pop()
}
} else {
result.push(text[i])
}
}
return result.join('')
}
function extractLastCdPath(text) {
const regex = /cd\s+([^\s]+)(?=\s|$)/g
let lastMatch
let match
regex.lastIndex = 0
while ((match = regex.exec(text)) !== null) {
lastMatch = match
}
return lastMatch ? lastMatch[1] : null
}
const onData = () => {
term.value.onData((key) => {
if ('\r' === key && curStatus.value === CONNECT_FAIL) {
@@ -522,28 +498,6 @@ const onData = () => {
if (acsiiCode === 6) return showSearchBar() // Ctrl + F
enterTimer.value = setTimeout(() => {
if (enterTimer.value) clearTimeout(enterTimer.value)
if (key === '\r') { // Enter
if (curStatus.value === CONNECT_SUCCESS) {
let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
const lines = cleanText.split('\n')
// console.log('lines: ', lines)
const lastLine = lines[lines.length - 1].trim()
// console.log('lastLine: ', lastLine)
// 截取最后一个提示符后的内容('$'或'#'后的内容)
const commandStartIndex = lastLine.lastIndexOf('#') + 1
const commandText = lastLine.substring(commandStartIndex).trim()
// eslint-disable-next-line
const cdPath = extractLastCdPath(commandText)
if (cdPath) {
console.log('cd command path:', cdPath)
let firstChar = cdPath.charAt(0)
if (!['/',].includes(firstChar)) return console.log('err fullpath:', cdPath) // 后端依赖不支持 '~'
emit('cdCommand', cdPath)
}
terminalText.value = ''
}
}
})
if (curStatus.value !== CONNECT_SUCCESS) return
emit('inputCommand', key, uid)
@@ -590,6 +544,69 @@ const plusTips = () => {
return true
}
const syncPathToSftp = () => {
if (curStatus.value !== CONNECT_SUCCESS) {
$message.warning('终端未连接')
return
}
// 监听一次输出,提取路径
let outputBuffer = ''
let pathFound = false
tempPathSyncCallback.value = (str) => {
outputBuffer += str
// 清理ANSI转义序列
// eslint-disable-next-line no-control-regex
const cleanBuffer = outputBuffer.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '')
// 查找路径(以/开头的行)
const lines = cleanBuffer.split(/\r?\n/)
for (const line of lines) {
const trimmed = line.trim()
// 匹配路径:以/开头,长度合理
if (trimmed.startsWith('/') && !pathFound && trimmed.length > 1 && trimmed.length < 500) {
// 进一步清理:只保留路径部分,去除可能的提示符
const pathMatch = trimmed.match(/^(\/[^\s]*?)(?:\s|$)/)
if (pathMatch) {
const cleanPath = pathMatch[1]
pathFound = true
// 找到路径,通过emit发送给父组件
emit('sync-path-to-sftp', cleanPath)
tempPathSyncCallback.value = null
$message.success(`已同步路径: ${ cleanPath }`)
break
}
}
}
// 限制缓冲区大小,防止无限增长
if (outputBuffer.length > 1000) {
tempPathSyncCallback.value = null
if (!pathFound) {
$message.error('未能获取路径')
}
}
}
// 设置超时,防止一直监听
setTimeout(() => {
if (tempPathSyncCallback.value) {
tempPathSyncCallback.value = null
if (!pathFound) {
$message.error('同步路径超时')
}
}
}, 3000)
// 延迟发送pwd命令
setTimeout(() => {
socket.value.emit('input', 'pwd\n')
}, 100)
}
const handleRightClick = async (e) => {
let str = term.value.getSelection().trim()
const sendToAI = str ? {
@@ -641,6 +658,14 @@ const handleRightClick = async (e) => {
focusTab()
}
}
const syncToSftp = props.showSftpSide ? {
label: '同步目录到SFTP',
onClick: () => {
syncPathToSftp()
term.value.clearSelection()
focusTab()
}
} : null
const dockerId = isDockerId(str) ? {
label: 'docker容器ID',
children: [
@@ -764,6 +789,7 @@ const handleRightClick = async (e) => {
search,
clear,
reconnect,
syncToSftp,
dockerId,
dockerComposeYml,
].filter(Boolean)
@@ -850,6 +876,7 @@ onBeforeUnmount(() => {
socket.value?.close()
window.removeEventListener('resize', handleResize)
clearInterval(pingTimer.value)
tempPathSyncCallback.value = null
})
defineExpose({