diff --git a/docs/17-sidebar-fixes.md b/docs/17-sidebar-fixes.md new file mode 100644 index 0000000..e92b9eb --- /dev/null +++ b/docs/17-sidebar-fixes.md @@ -0,0 +1,33 @@ +# 侧边栏四项反馈处理(2026-11-14) + +对应大王截图反馈:①「打开文件夹」按钮偏了 ②没进上次打开的文件夹 ③没有下级文件夹 ④不能在文件夹下新建文件夹和文档。验证:tsc 0 错误、vitest **86 通过 + 2 跳过**(新增文件树回归 3 例)、build 通过。 + +## ① 按钮偏移 + +空状态区(.sidebar-empty)改为与页签行一致的 10px 左缩进、纵向 flex 左对齐排布,段落默认外边距清零,按钮与上方文字左缘对齐、不再贴边/溢出。 + +## ② 启动自动恢复上次工作区 + +原先只把打开过的工作区写进历史表、启动时从不读回。现:打开文件夹时把路径存入偏好(lastWorkspace);启动初始化时读回并校验文件夹仍存在(statFile),存在就自动设为工作区根。不新增 Rust 命令(复用 prefs 通道),Web 模式走 localStorage 同构。 + +## ③ 下级文件夹 + +实测结论先说清:**当前代码的文件树本来就支持逐级展开**——专门写了组件级测试(mock 目录数据)验证:文件夹行带 ▸,点击展开能看到下级 .md,逻辑没有缺陷。用户当时看不到下级,最可能两种情况:测试时跑的构建较旧;或子文件夹里没有 .md/.markdown/.txt 文件——文件树按设计只收这三类文档,其余文件不显示,展开后就是空白,容易误读为「没有下级」。 + +本次做的改进是把反馈做明确,避免再误读: +- 展开后的空文件夹显示一行「(空)」而不是空白; +- 文件夹行加粗区分,▸/▾ 展开箭头保留。 + +## ④ 在任意文件夹下新建 + +- 树头两个按钮:「+ 文档」「+ 文件夹」,作用于工作区根目录; +- 文件夹右键菜单新增「在此新建文档…」「在此新建文件夹…」,作用于该文件夹; +- 新建文档沿用原逻辑(补 .md、重名报错不覆盖、建完自动打开);新建文件夹校验名字(不能为空、不能含 Windows 非法字符),建完刷新树并提示。 +- 实现:App 的 createFileInWorkspace 泛化为 createFileInDir(dir),新增 createFolderInDir(dir)(走既有 createDirPath 命令)。 + +## 复验建议(Windows) + +- 重启应用:应直接进上次的文件夹,文件树就位。 +- 点开任意子文件夹:有文档的显示文档,没有的显示「(空)」。 +- 右键一个子文件夹 → 在此新建文档/文件夹:建完树里立刻出现,新文档自动打开。 +- 关掉工作区(或新装首次)看空状态:按钮与「尚未打开文件夹。」文字左缘对齐。 diff --git a/src/App.tsx b/src/App.tsx index 729f787..ced895d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,7 +26,7 @@ import { isTauri, pickOpenFile, pickSaveFile, pickFolder, readTextFile, writeTextFile, pickAndReadWebFile, downloadTextFile, pickImageFile, pickAnyFile, pickAndReadWebImage, copyToAssets, saveBytesToAssets, fileToBase64, - statFile, createTextFile, renamePath, deleteToTrash, type DirEntry, type RecoveryEntry, + statFile, createTextFile, createDirPath, renamePath, deleteToTrash, type DirEntry, type RecoveryEntry, pickSaveHtmlFile, pickImportFile, readFileBase64, readBinaryFile, pickAndReadWebImport, } from './services/platform' import * as store from './services/store' @@ -423,6 +423,13 @@ export default function App() { uiLoadedRef.current = true const recentFromDb = await store.loadRecent() if (!cancelled && recentFromDb) setRecent(recentFromDb.map((r) => r.path)) + // 工作区恢复:自动回到上次打开的文件夹(文件夹已不存在则跳过) + const lastWs = await store.loadLastWorkspace() + if (!cancelled && lastWs) { + try { + if ((await statFile(lastWs)).exists) setWorkspaceRoot(lastWs) + } catch { /* 已删除/不可达时不恢复 */ } + } // 启动恢复:先读磁盘现状再划分(与磁盘一致的记录静默清理) const records = await store.listRecovery() if (!cancelled && records.length > 0) { @@ -447,7 +454,10 @@ export default function App() { }, []) useEffect(() => { - if (workspaceRoot) store.addWorkspace(workspaceRoot) + if (workspaceRoot) { + store.addWorkspace(workspaceRoot) + store.saveLastWorkspace(workspaceRoot) + } }, [workspaceRoot]) // ——— 阶段 4:标签关闭后清理基线与恢复记录 ——— @@ -576,14 +586,13 @@ export default function App() { Boolean(a && b && a.replace(/\\/g, '/') === b.replace(/\\/g, '/')) const dirOf = (path: string) => path.replace(/[\\/][^\\/]*$/, '') - const createFileInWorkspace = useCallback(() => { - if (!workspaceRoot) return + const createFileInDir = useCallback((dir: string) => { setPromptSpec({ - title: '在工作区新建文档', label: '文件名', initial: '未命名.md', + title: '新建文档', label: '文件名', initial: '未命名.md', submit: (name) => { setPromptSpec(null) const fileName = /\.(md|markdown|txt)$/i.test(name) ? name : `${name}.md` - const path = joinPath(workspaceRoot, fileName) + const path = joinPath(dir, fileName) void (async () => { try { await createTextFile(path) @@ -593,7 +602,29 @@ export default function App() { })() }, }) - }, [workspaceRoot, openPath]) + }, [openPath ]) + + const createFolderInDir = useCallback((dir: string) => { + setPromptSpec({ + title: '新建文件夹', label: '文件夹名', initial: '新文件夹', + submit: (name) => { + setPromptSpec(null) + const trimmed = name.trim() + if (!trimmed || /[\\/:*?"<>|]/.test(trimmed)) { + setError('文件夹名不合法:不能为空,也不能包含 \\ / : * ? " < > |') + return + } + const path = joinPath(dir, trimmed) + void (async () => { + try { + await createDirPath(path) + setTreeRefresh((n) => n + 1) + flash(`已新建文件夹:${trimmed}`) + } catch (e) { setError(String(e)) } + })() + }, + }) + }, [flash]) const renameEntry = useCallback((entry: DirEntry) => { setPromptSpec({ @@ -1116,7 +1147,8 @@ export default function App() { activePath={tab?.path ?? null} onOpenFile={handleSidebarOpenFile} onOpenFolder={handleSidebarOpenFolder} - onCreateFile={tauriMode && workspaceRoot ? createFileInWorkspace : undefined} + onCreateFileIn={tauriMode && workspaceRoot ? createFileInDir : undefined} + onCreateFolderIn={tauriMode && workspaceRoot ? createFolderInDir : undefined} onRenameEntry={tauriMode ? renameEntry : undefined} onDeleteEntry={tauriMode ? deleteEntry : undefined} refreshSignal={treeRefresh} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 898c3f5..2b3c57c 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -11,7 +11,8 @@ interface Props { activePath: string | null onOpenFile: (path: string) => void onOpenFolder: () => void - onCreateFile?: () => void + onCreateFileIn?: (dir: string) => void + onCreateFolderIn?: (dir: string) => void onRenameEntry?: (entry: DirEntry) => void onDeleteEntry?: (entry: DirEntry) => void refreshSignal?: number @@ -51,7 +52,8 @@ function Sidebar(p: Props) { root={p.workspaceRoot} activePath={p.activePath} onOpenFile={p.onOpenFile} - onCreateFile={p.onCreateFile} + onCreateFileIn={p.onCreateFileIn} + onCreateFolderIn={p.onCreateFolderIn} onRenameEntry={p.onRenameEntry} onDeleteEntry={p.onDeleteEntry} refreshSignal={p.refreshSignal ?? 0} @@ -68,11 +70,12 @@ function Sidebar(p: Props) { ) } -function FileTree({ root, activePath, onOpenFile, onCreateFile, onRenameEntry, onDeleteEntry, refreshSignal }: { +function FileTree({ root, activePath, onOpenFile, onCreateFileIn, onCreateFolderIn, onRenameEntry, onDeleteEntry, refreshSignal }: { root: string activePath: string | null onOpenFile: (path: string) => void - onCreateFile?: () => void + onCreateFileIn?: (dir: string) => void + onCreateFolderIn?: (dir: string) => void onRenameEntry?: (entry: DirEntry) => void onDeleteEntry?: (entry: DirEntry) => void refreshSignal: number @@ -92,7 +95,8 @@ function FileTree({ root, activePath, onOpenFile, onCreateFile, onRenameEntry, o
{baseName(root)} - {onCreateFile && } + {onCreateFileIn && } + {onCreateFolderIn && }
e.stopPropagation()}> {!menu.entry.isDir && } + {menu.entry.isDir && onCreateFileIn && } + {menu.entry.isDir && onCreateFolderIn && } {onRenameEntry && } {onDeleteEntry && }
@@ -147,10 +153,11 @@ function TreeLevel({ dir, depth, activePath, onOpenFile, refreshKey, onEntryMenu return (
+ {entries.length === 0 &&
(空)
} {entries.map((entry) => (