Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
55cc5460
Unverified
提交
55cc5460
authored
8月 16, 2025
作者:
Will Chen
提交者:
GitHub
8月 16, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Support next.js for routes and handle long width address bar (#958)
上级
e554fd96
隐藏空白字符变更
内嵌
并排
正在显示
2 个修改的文件
包含
173 行增加
和
50 行删除
+173
-50
PreviewIframe.tsx
src/components/preview_panel/PreviewIframe.tsx
+6
-50
useParseRouter.ts
src/hooks/useParseRouter.ts
+167
-0
没有找到文件。
src/components/preview_panel/PreviewIframe.tsx
浏览文件 @
55cc5460
...
@@ -23,7 +23,7 @@ import {
...
@@ -23,7 +23,7 @@ import {
import
{
selectedChatIdAtom
}
from
"@/atoms/chatAtoms"
;
import
{
selectedChatIdAtom
}
from
"@/atoms/chatAtoms"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
use
LoadAppFile
}
from
"@/hooks/useLoadAppFile
"
;
import
{
use
ParseRouter
}
from
"@/hooks/useParseRouter
"
;
import
{
import
{
DropdownMenu
,
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuContent
,
...
@@ -128,52 +128,8 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
...
@@ -128,52 +128,8 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
const
[
errorMessage
,
setErrorMessage
]
=
useAtom
(
previewErrorMessageAtom
);
const
[
errorMessage
,
setErrorMessage
]
=
useAtom
(
previewErrorMessageAtom
);
const
selectedChatId
=
useAtomValue
(
selectedChatIdAtom
);
const
selectedChatId
=
useAtomValue
(
selectedChatIdAtom
);
const
{
streamMessage
}
=
useStreamChat
();
const
{
streamMessage
}
=
useStreamChat
();
const
[
availableRoutes
,
setAvailableRoutes
]
=
useState
<
const
{
routes
:
availableRoutes
}
=
useParseRouter
(
selectedAppId
);
Array
<
{
path
:
string
;
label
:
string
}
>
>
([]);
const
{
restartApp
}
=
useRunApp
();
const
{
restartApp
}
=
useRunApp
();
// Load router related files to extract routes
const
{
content
:
routerContent
}
=
useLoadAppFile
(
selectedAppId
,
"src/App.tsx"
,
);
// Effect to parse routes from the router file
useEffect
(()
=>
{
if
(
routerContent
)
{
try
{
const
routes
:
Array
<
{
path
:
string
;
label
:
string
}
>
=
[];
// Extract route imports and paths using regex for React Router syntax
// Match <Route path="/path">
const
routePathsRegex
=
/<Route
\s
+
(?:[^
>
]
*
\s
+
)?
path=
[
"'
]([^
"'
]
+
)[
"'
]
/g
;
let
match
;
// Find all route paths in the router content
while
((
match
=
routePathsRegex
.
exec
(
routerContent
))
!==
null
)
{
const
path
=
match
[
1
];
// Create a readable label from the path
const
label
=
path
===
"/"
?
"Home"
:
path
.
split
(
"/"
)
.
filter
((
segment
)
=>
segment
&&
!
segment
.
startsWith
(
":"
))
.
pop
()
?.
replace
(
/
[
-_
]
/g
,
" "
)
.
replace
(
/^
\w
/
,
(
c
)
=>
c
.
toUpperCase
())
||
path
;
if
(
!
routes
.
some
((
r
)
=>
r
.
path
===
path
))
{
routes
.
push
({
path
,
label
});
}
}
setAvailableRoutes
(
routes
);
}
catch
(
e
)
{
console
.
error
(
"Error parsing router file:"
,
e
);
}
}
},
[
routerContent
]);
// Navigation state
// Navigation state
const
[
isComponentSelectorInitialized
,
setIsComponentSelectorInitialized
]
=
const
[
isComponentSelectorInitialized
,
setIsComponentSelectorInitialized
]
=
...
@@ -502,17 +458,17 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
...
@@ -502,17 +458,17 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
</
div
>
</
div
>
{
/* Address Bar with Routes Dropdown - using shadcn/ui dropdown-menu */
}
{
/* Address Bar with Routes Dropdown - using shadcn/ui dropdown-menu */
}
<
div
className=
"relative flex-grow"
>
<
div
className=
"relative flex-grow
min-w-20
"
>
<
DropdownMenu
>
<
DropdownMenu
>
<
DropdownMenuTrigger
asChild
>
<
DropdownMenuTrigger
asChild
>
<
div
className=
"flex items-center justify-between px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm text-gray-700 dark:text-gray-200 cursor-pointer w-full"
>
<
div
className=
"flex items-center justify-between px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm text-gray-700 dark:text-gray-200 cursor-pointer w-full
min-w-0
"
>
<
span
>
<
span
className=
"truncate flex-1 mr-2 min-w-0"
>
{
navigationHistory
[
currentHistoryPosition
]
{
navigationHistory
[
currentHistoryPosition
]
?
new
URL
(
navigationHistory
[
currentHistoryPosition
])
?
new
URL
(
navigationHistory
[
currentHistoryPosition
])
.
pathname
.
pathname
:
"/"
}
:
"/"
}
</
span
>
</
span
>
<
ChevronDown
size=
{
14
}
/>
<
ChevronDown
size=
{
14
}
className=
"flex-shrink-0"
/>
</
div
>
</
div
>
</
DropdownMenuTrigger
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
className=
"w-full"
>
<
DropdownMenuContent
className=
"w-full"
>
...
...
src/hooks/useParseRouter.ts
0 → 100644
浏览文件 @
55cc5460
import
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
{
useLoadAppFile
}
from
"@/hooks/useLoadAppFile"
;
import
{
useLoadApp
}
from
"@/hooks/useLoadApp"
;
export
interface
ParsedRoute
{
path
:
string
;
label
:
string
;
}
/**
* Loads the app router file and parses available routes for quick navigation.
*/
export
function
useParseRouter
(
appId
:
number
|
null
)
{
const
[
routes
,
setRoutes
]
=
useState
<
ParsedRoute
[]
>
([]);
// Load app to access the file list
const
{
app
,
loading
:
appLoading
,
error
:
appError
,
refreshApp
,
}
=
useLoadApp
(
appId
);
// Load router related file to extract routes for non-Next apps
const
{
content
:
routerContent
,
loading
:
routerFileLoading
,
error
:
routerFileError
,
refreshFile
,
}
=
useLoadAppFile
(
appId
,
"src/App.tsx"
);
// Detect Next.js app by presence of next.config.* in file list
const
isNextApp
=
useMemo
(()
=>
{
if
(
!
app
?.
files
)
return
false
;
return
app
.
files
.
some
((
f
)
=>
f
.
toLowerCase
().
includes
(
"next.config"
));
},
[
app
?.
files
]);
// Parse routes either from Next.js file-based routing or from router file
useEffect
(()
=>
{
const
buildLabel
=
(
path
:
string
)
=>
path
===
"/"
?
"Home"
:
path
.
split
(
"/"
)
.
filter
((
segment
)
=>
segment
&&
!
segment
.
startsWith
(
":"
))
.
pop
()
?.
replace
(
/
[
-_
]
/g
,
" "
)
.
replace
(
/^
\w
/
,
(
c
)
=>
c
.
toUpperCase
())
||
path
;
const
setFromNextFiles
=
(
files
:
string
[])
=>
{
const
nextRoutes
=
new
Set
<
string
>
();
// pages directory (pages router)
const
pageFileRegex
=
/^
(?:
pages
)\/(
.+
)\.(?:
js|jsx|ts|tsx|mdx
)
$/i
;
for
(
const
file
of
files
)
{
if
(
!
file
.
startsWith
(
"pages/"
))
continue
;
if
(
file
.
startsWith
(
"pages/api/"
))
continue
;
// skip api routes
const
baseName
=
file
.
split
(
"/"
).
pop
()
||
""
;
if
(
baseName
.
startsWith
(
"_"
))
continue
;
// _app, _document, etc.
const
m
=
file
.
match
(
pageFileRegex
);
if
(
!
m
)
continue
;
let
routePath
=
m
[
1
];
// Ignore dynamic routes containing [ ]
if
(
routePath
.
includes
(
"["
))
continue
;
// Normalize index files
if
(
routePath
===
"index"
)
{
nextRoutes
.
add
(
"/"
);
continue
;
}
if
(
routePath
.
endsWith
(
"/index"
))
{
routePath
=
routePath
.
slice
(
0
,
-
"/index"
.
length
);
}
nextRoutes
.
add
(
"/"
+
routePath
);
}
// app directory (app router)
const
appPageRegex
=
/^
(?:
src
\/)?
app
\/(
.*
)\/
page
\.(?:
js|jsx|ts|tsx|mdx
)
$/i
;
for
(
const
file
of
files
)
{
const
lower
=
file
.
toLowerCase
();
if
(
lower
===
"app/page.tsx"
||
lower
===
"app/page.jsx"
||
lower
===
"app/page.js"
||
lower
===
"app/page.mdx"
||
lower
===
"app/page.ts"
||
lower
===
"src/app/page.tsx"
||
lower
===
"src/app/page.jsx"
||
lower
===
"src/app/page.js"
||
lower
===
"src/app/page.mdx"
||
lower
===
"src/app/page.ts"
)
{
nextRoutes
.
add
(
"/"
);
continue
;
}
const
m
=
file
.
match
(
appPageRegex
);
if
(
!
m
)
continue
;
const
routeSeg
=
m
[
1
];
// Ignore dynamic segments and grouping folders like (marketing)
if
(
routeSeg
.
includes
(
"["
))
continue
;
const
cleaned
=
routeSeg
.
split
(
"/"
)
.
filter
((
s
)
=>
s
&&
!
s
.
startsWith
(
"("
))
.
join
(
"/"
);
if
(
!
cleaned
)
{
nextRoutes
.
add
(
"/"
);
}
else
{
nextRoutes
.
add
(
"/"
+
cleaned
);
}
}
const
parsed
=
Array
.
from
(
nextRoutes
).
map
((
path
)
=>
({
path
,
label
:
buildLabel
(
path
),
}));
setRoutes
(
parsed
);
};
const
setFromRouterFile
=
(
content
:
string
|
null
)
=>
{
if
(
!
content
)
{
setRoutes
([]);
return
;
}
try
{
const
parsedRoutes
:
ParsedRoute
[]
=
[];
const
routePathsRegex
=
/<Route
\s
+
(?:[^
>
]
*
\s
+
)?
path=
[
"'
]([^
"'
]
+
)[
"'
]
/g
;
let
match
:
RegExpExecArray
|
null
;
while
((
match
=
routePathsRegex
.
exec
(
content
))
!==
null
)
{
const
path
=
match
[
1
];
const
label
=
buildLabel
(
path
);
if
(
!
parsedRoutes
.
some
((
r
)
=>
r
.
path
===
path
))
{
parsedRoutes
.
push
({
path
,
label
});
}
}
setRoutes
(
parsedRoutes
);
}
catch
(
e
)
{
console
.
error
(
"Error parsing router file:"
,
e
);
setRoutes
([]);
}
};
if
(
isNextApp
&&
app
?.
files
)
{
setFromNextFiles
(
app
.
files
);
}
else
{
setFromRouterFile
(
routerContent
??
null
);
}
},
[
isNextApp
,
app
?.
files
,
routerContent
]);
const
combinedLoading
=
appLoading
||
routerFileLoading
;
const
combinedError
=
appError
||
routerFileError
||
null
;
const
refresh
=
async
()
=>
{
await
Promise
.
allSettled
([
refreshApp
(),
refreshFile
()]);
};
return
{
routes
,
loading
:
combinedLoading
,
error
:
combinedError
,
refreshFile
:
refresh
,
};
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论