diff --git a/web/src/components/LocalDirBrowserDialog.tsx b/web/src/components/LocalDirBrowserDialog.tsx
new file mode 100644
index 0000000..8146852
--- /dev/null
+++ b/web/src/components/LocalDirBrowserDialog.tsx
@@ -0,0 +1,145 @@
+import { useEffect, useState } from 'react'
+import { ChevronRight, Folder, FolderPlus, Loader2, X } from 'lucide-react'
+import toast from 'react-hot-toast'
+
+import { strmAPI } from '../api/strm'
+
+function apiErrorMessage(err: unknown): string {
+ if (typeof err === 'object' && err !== null && 'response' in err) {
+ const res = (err as { response?: { data?: { error?: string; message?: string } } }).response
+ if (res?.data?.error) return res.data.error
+ if (res?.data?.message) return res.data.message
+ }
+ if (err instanceof Error) return err.message
+ return '请求失败'
+}
+
+export function LocalDirBrowserDialog({
+ initialDir,
+ title = '选择本地目录',
+ onSelect,
+ onClose,
+}: {
+ initialDir?: string
+ title?: string
+ onSelect: (path: string) => void
+ onClose: () => void
+}) {
+ const [list, setList] = useState<{
+ roots: boolean
+ parent?: string
+ current?: string
+ children: { name: string; path: string }[]
+ } | null>(null)
+ const [loading, setLoading] = useState(true)
+
+ const load = async (target: string) => {
+ setLoading(true)
+ try {
+ const data = await strmAPI.listLocalDirs(target)
+ setList(data)
+ } catch (err) {
+ toast.error(apiErrorMessage(err))
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ load(initialDir ?? '').catch(() => undefined)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const selectable = list && !list.roots && list.current
+
+ return (
+
+
event.stopPropagation()}
+ >
+
+
{title}
+
+
+
+ {list?.roots ? (
+ 选择盘符 / 根目录
+ ) : (
+ <>
+
+
+
+ {list?.current}
+
+ >
+ )}
+
+
+ {loading ? (
+
+
+
+ ) : !list || list.children.length === 0 ? (
+
该目录下没有子目录
+ ) : (
+
+ {(list.parent ?? '') !== '' && (
+
+ )}
+ {list.children.map((entry) => (
+
+ ))}
+
+ )}
+
+
+ 单击目录进入下一级,点击「选择当前目录」完成选择
+
+
+
+
+ )
+}
diff --git a/web/src/pages/AdminLibraryPanelSections.tsx b/web/src/pages/AdminLibraryPanelSections.tsx
index a3b3576..1833c3d 100644
--- a/web/src/pages/AdminLibraryPanelSections.tsx
+++ b/web/src/pages/AdminLibraryPanelSections.tsx
@@ -1,6 +1,7 @@
-import { FormEvent } from 'react'
-import { Plus, Trash2 } from 'lucide-react'
+import { FormEvent, useState } from 'react'
+import { Folder, Plus, Trash2 } from 'lucide-react'
+import { LocalDirBrowserDialog } from '../components/LocalDirBrowserDialog'
import type { RootDraft } from './adminLibraryPanelModel'
type CreateFormProps = {
@@ -30,51 +31,77 @@ export function AdminLibraryCreateForm({
onRemoveRoot,
onSubmit,
}: CreateFormProps) {
+ const [browsingIndex, setBrowsingIndex] = useState(null)
+
+ const handleSelectDir = (selectedPath: string) => {
+ if (browsingIndex === null) return
+ const currentRoot = roots[browsingIndex]
+ const segments = selectedPath.split(/[/\\]/).filter(Boolean)
+ const baseName = segments.length > 0 ? segments[segments.length - 1] : ''
+ const patch: Partial = { path: selectedPath }
+ if (!currentRoot?.name?.trim() && baseName) {
+ patch.name = baseName
+ }
+ onRootChange(browsingIndex, patch)
+ setBrowsingIndex(null)
+ }
+
return (
-
+
+
+ {browsingIndex !== null && (
+ setBrowsingIndex(null)}
+ />
+ )}
+ >
)
}
@@ -83,25 +110,35 @@ type CreateRootRowProps = {
index: number
canRemove: boolean
onChange: (index: number, patch: Partial) => void
+ onBrowse: () => void
onRemove: (index: number) => void
}
-function CreateRootRow({ root, index, canRemove, onChange, onRemove }: CreateRootRowProps) {
+function CreateRootRow({ root, index, canRemove, onChange, onBrowse, onRemove }: CreateRootRowProps) {
return (
-
+
onChange(index, { name: e.target.value })}
/>
onChange(index, { path: e.target.value })}
/>
+