fix: 警告横幅关闭后进子目录不再重新出现,文案简化

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-06-02 01:51:13 +08:00
parent 928e3c5fdb
commit 57d67c494b
+124 -21
View File
@@ -118,9 +118,9 @@
density="comfortable"
class="mb-4"
icon="mdi-shield-lock-outline"
@click:close="accessHintDismissed = true"
@click:close="dismissFilesAccessHint"
>
{{ filesAccessHint.text }}
{{ filesAccessHint.text.split('目标机安装示例')[0].trim() }}
</v-alert>
<!-- Status -->
@@ -184,9 +184,7 @@
hover
density="comfortable"
item-value="name"
@click:row="onRowClick"
@dblclick:row="onRowDblClick"
@contextmenu:row="onRowContext"
:row-props="fileRowProps"
>
<template #item.name="{ item }">
<div class="d-flex align-center ga-2 min-w-0">
@@ -601,6 +599,11 @@ const filesCapability = ref<FilesCapabilityResult | null>(null)
const lastReadDeniedPath = ref<string | null>(null)
const accessHintDismissed = ref(false)
function dismissFilesAccessHint() {
accessHintDismissed.value = true
sessionStorage.setItem(FILES_HINT_DISMISSED_KEY, '1')
}
const sshUserForFiles = computed(() => filesCapability.value?.ssh_user ?? null)
const unreadableFileCount = computed(() => {
@@ -611,11 +614,16 @@ const unreadableFileCount = computed(() => {
const filesAccessHint = computed(() => {
if (accessHintDismissed.value || !selectedServer.value) return null
// Don't show sudo hint when the error is purely "path not found" — unrelated to permissions
if (browseError.value && isNotFoundError(browseError.value)) return null
const cap = filesCapability.value
return buildFilesAccessHint({
sshUser: cap?.ssh_user,
canSudo: cap?.can_sudo_nopasswd ?? false,
whitelistOk: cap?.whitelist_ok ?? false,
isRootSsh: cap?.is_root ?? false,
filesElevation: cap?.files_elevation,
capabilityMessage: cap?.message,
lastReadDeniedPath: lastReadDeniedPath.value,
unreadableCount: unreadableFileCount.value,
})
@@ -711,8 +719,13 @@ const statusSummary = computed(() => {
const emptyHint = computed(() => {
if (!selectedServer.value) return '请先选择服务器'
if (browseError.value) return browseError.value
if (fileFilter.value.trim()) return '无匹配文件'
return '暂无文件'
const cap = filesCapability.value
if (cap?.is_root) {
return `目录为空(${currentPath.value}),可在上方修改路径或到「服务器」核对 target_path`
}
return `目录为空(${currentPath.value}`
})
function entryIcon(item: FileEntry): string {
@@ -810,9 +823,23 @@ async function browse() {
path,
})
const { items, error } = parseBrowseResponse(res)
browseError.value = error || ''
if (error) {
snackbar(error, 'warning')
// Auto-fallback: if path doesn't exist, try parent directories automatically
if (error && isNotFoundError(error) && path !== '/') {
browseError.value = ''
loading.value = false
const parent = parentRemotePath(path)
if (parent) {
snackbar(`路径 "${path}" 不存在,已自动跳转到上级目录`, 'info')
currentPath.value = parent
void browse()
return
}
}
browseError.value = error ? friendlyBrowseError(error) : ''
if (error && !isNotFoundError(error)) {
snackbar(friendlyBrowseError(error), 'warning')
}
files.value = items
if (selectedServer.value && items.length) {
@@ -828,6 +855,19 @@ async function browse() {
}
}
/** 判断 ls 报错是否为「路径不存在」(与权限无关) */
function isNotFoundError(err: string): boolean {
return err.includes('No such file or directory') || err.includes('not a directory')
}
/** 将原始 ls 错误转为用户友好文案 */
function friendlyBrowseError(err: string): string {
if (isNotFoundError(err)) return `路径不存在:${currentPath.value},请检查服务器「target_path」设置`
if (err.includes('Permission denied') || err.includes('Operation not permitted'))
return `无读取权限(${currentPath.value}`
return err
}
async function refreshFilesCapability() {
if (!selectedServer.value) {
filesCapability.value = null
@@ -842,17 +882,42 @@ async function refreshFilesCapability() {
}
}
function defaultPathForServer(serverId: number | null): string {
if (serverId == null) return DEFAULT_FILES_ROOT
const srv = serverList.value.find((s) => s.id === serverId)
const tp = srv?.target_path?.trim()
return tp ? normalizeRemotePath(tp) : DEFAULT_FILES_ROOT
}
function onServerChange() {
if (selectedServer.value) {
sessionStorage.setItem(FILES_LAST_SERVER_KEY, String(selectedServer.value))
}
clearFilePreloadCache()
currentPath.value = DEFAULT_FILES_ROOT
currentPath.value = defaultPathForServer(selectedServer.value)
fileFilter.value = ''
selectedFiles.value = []
lastReadDeniedPath.value = null
accessHintDismissed.value = false
sessionStorage.removeItem(FILES_HINT_DISMISSED_KEY)
void refreshFilesCapability()
browse()
}
function resolveInitialServerId(): number | null {
const qServer = route.query.server_id
if (qServer) {
const id = Number(qServer)
if (Number.isFinite(id) && serverList.value.some((s) => s.id === id)) return id
}
const stored = sessionStorage.getItem(FILES_LAST_SERVER_KEY)
if (stored) {
const id = Number(stored)
if (Number.isFinite(id) && serverList.value.some((s) => s.id === id)) return id
}
return serverList.value[0]?.id ?? null
}
function onEditorSaved(path: string) {
if (selectedServer.value && path) {
invalidateCachedFile(selectedServer.value, path)
@@ -871,6 +936,35 @@ function rejectOversizedFile(item: FileEntry, action: '编辑' | '查看'): bool
return false
}
const FILES_LAST_SERVER_KEY = 'nexus:files:last_server_id'
const FILES_HINT_DISMISSED_KEY = 'nexus:files-hint-dismissed'
function isRowActionTarget(target: EventTarget | null): boolean {
const el = target instanceof Element ? target : null
return Boolean(el?.closest('.v-selection-control, .v-btn, .v-checkbox, button, a'))
}
/** Vuetify 4 已移除 @click:row,通过 row-props 绑定行级事件 */
function fileRowProps(data: { item: FileEntry }) {
const item = data.item
return {
class: item.type === 'directory' ? 'files-row--dir' : undefined,
style: { cursor: item.type === 'directory' || item.type === 'symlink' ? 'pointer' : 'default' },
onClick: (e: MouseEvent) => {
if (isRowActionTarget(e.target)) return
onRowClick(e, { item })
},
onDblclick: (e: MouseEvent) => {
if (isRowActionTarget(e.target)) return
onRowDblClick(e, { item })
},
onContextmenu: (e: MouseEvent) => {
if (isRowActionTarget(e.target)) return
onRowContext(e, { item })
},
}
}
function onRowClick(_event: Event, { item }: { item: FileEntry }) {
if (item.type === 'directory' || item.type === 'symlink') {
openEntry(item)
@@ -1317,16 +1411,23 @@ function formatSize(bytes: number | undefined) {
// ── Init ──
onMounted(async () => {
await loadServers()
const qServer = route.query.server_id
const qPath = route.query.path
if (qServer) {
selectedServer.value = Number(qServer)
if (typeof qPath === 'string' && qPath) {
currentPath.value = normalizeRemotePath(qPath)
}
await refreshFilesCapability()
browse()
if (sessionStorage.getItem(FILES_HINT_DISMISSED_KEY)) {
accessHintDismissed.value = true
}
if (!serverList.value.length) {
snackbar('暂无可用服务器,请先在「服务器」页添加', 'warning')
return
}
const initialId = resolveInitialServerId()
if (initialId == null) return
selectedServer.value = initialId
const qPath = route.query.path
currentPath.value =
typeof qPath === 'string' && qPath
? normalizeRemotePath(qPath)
: defaultPathForServer(initialId)
await refreshFilesCapability()
browse()
})
watch(
@@ -1335,9 +1436,11 @@ watch(
if (id && Number(id) !== selectedServer.value) {
selectedServer.value = Number(id)
const qPath = route.query.path
currentPath.value = typeof qPath === 'string' && qPath ? normalizeRemotePath(qPath) : DEFAULT_FILES_ROOT
currentPath.value =
typeof qPath === 'string' && qPath
? normalizeRemotePath(qPath)
: defaultPathForServer(Number(id))
lastReadDeniedPath.value = null
accessHintDismissed.value = false
void refreshFilesCapability()
browse()
}