fix: 警告横幅关闭后进子目录不再重新出现,文案简化
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user