mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-26 03:44:58 +08:00
fix: plugin pages scroll entire viewport instead of content area only (#1788)
* Initial plan * Fix scroll behavior in plugin pages - only content areas scroll now Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
This commit is contained in:
@@ -274,7 +274,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{pluginList.length === 0 ? (
|
{pluginList.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] w-full gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="h-[3rem] w-[3rem]"
|
className="h-[3rem] w-[3rem]"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ function MarketPageContent({
|
|||||||
|
|
||||||
const pageSize = 16; // 每页16个,4行x4列
|
const pageSize = 16; // 每页16个,4行x4列
|
||||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// 排序选项
|
// 排序选项
|
||||||
const sortOptions: SortOption[] = [
|
const sortOptions: SortOption[] = [
|
||||||
@@ -262,19 +263,21 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
}, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]);
|
}, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]);
|
||||||
|
|
||||||
// 监听滚动事件
|
// Listen to scroll events on the scroll container
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (
|
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
||||||
window.innerHeight + document.documentElement.scrollTop >=
|
// Load more when scrolled to within 100px of the bottom
|
||||||
document.documentElement.offsetHeight - 100
|
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||||
) {
|
|
||||||
loadMore();
|
loadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
scrollContainer.addEventListener('scroll', handleScroll);
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
||||||
}, [loadMore]);
|
}, [loadMore]);
|
||||||
|
|
||||||
// 安装插件
|
// 安装插件
|
||||||
@@ -283,99 +286,109 @@ function MarketPageContent({
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-3 sm:px-4 py-4 sm:py-6 space-y-4 sm:space-y-6">
|
<div className="h-full flex flex-col">
|
||||||
{/* 搜索框 */}
|
{/* Fixed header with search and sort controls */}
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex-shrink-0 space-y-4 px-3 sm:px-4 py-4 sm:py-6">
|
||||||
<div className="relative w-full max-w-2xl">
|
{/* Search box */}
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
<div className="flex items-center justify-center">
|
||||||
<Input
|
<div className="relative w-full max-w-2xl">
|
||||||
placeholder={t('market.searchPlaceholder')}
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||||
value={searchQuery}
|
<Input
|
||||||
onChange={(e) => handleSearchInputChange(e.target.value)}
|
placeholder={t('market.searchPlaceholder')}
|
||||||
onKeyPress={(e) => {
|
value={searchQuery}
|
||||||
if (e.key === 'Enter') {
|
onChange={(e) => handleSearchInputChange(e.target.value)}
|
||||||
// 立即搜索,清除防抖定时器
|
onKeyPress={(e) => {
|
||||||
if (searchTimeoutRef.current) {
|
if (e.key === 'Enter') {
|
||||||
clearTimeout(searchTimeoutRef.current);
|
// Immediately search, clear debounce timer
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
handleSearch(searchQuery);
|
||||||
}
|
}
|
||||||
handleSearch(searchQuery);
|
}}
|
||||||
}
|
className="pl-10 pr-4 text-sm sm:text-base"
|
||||||
}}
|
/>
|
||||||
className="pl-10 pr-4 text-sm sm:text-base"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 排序下拉框 */}
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
|
|
||||||
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
|
||||||
{t('market.sortBy')}:
|
|
||||||
</span>
|
|
||||||
<Select value={sortOption} onValueChange={handleSortChange}>
|
|
||||||
<SelectTrigger className="w-40 sm:w-48 text-xs sm:text-sm">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{sortOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 搜索结果统计 */}
|
|
||||||
{total > 0 && (
|
|
||||||
<div className="text-center text-muted-foreground text-sm">
|
|
||||||
{searchQuery
|
|
||||||
? t('market.searchResults', { count: total })
|
|
||||||
: t('market.totalPlugins', { count: total })}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 插件列表 */}
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
<Loader2 className="h-8 w-8 animate-spin" />
|
|
||||||
<span className="ml-2">{t('market.loading')}</span>
|
|
||||||
</div>
|
|
||||||
) : plugins.length === 0 ? (
|
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
<div className="text-muted-foreground">
|
|
||||||
{searchQuery ? t('market.noResults') : t('market.noPlugins')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6">
|
|
||||||
{plugins.map((plugin) => (
|
|
||||||
<PluginMarketCardComponent
|
|
||||||
key={plugin.pluginId}
|
|
||||||
cardVO={plugin}
|
|
||||||
onPluginClick={handlePluginClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 加载更多指示器 */}
|
{/* Sort dropdown */}
|
||||||
{isLoadingMore && (
|
<div className="flex items-center justify-center">
|
||||||
<div className="flex items-center justify-center py-6">
|
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
||||||
<span className="ml-2">{t('market.loadingMore')}</span>
|
{t('market.sortBy')}:
|
||||||
|
</span>
|
||||||
|
<Select value={sortOption} onValueChange={handleSortChange}>
|
||||||
|
<SelectTrigger className="w-40 sm:w-48 text-xs sm:text-sm">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 没有更多数据提示 */}
|
{/* Search results stats */}
|
||||||
{!hasMore && plugins.length > 0 && (
|
{total > 0 && (
|
||||||
<div className="text-center text-muted-foreground py-6">
|
<div className="text-center text-muted-foreground text-sm">
|
||||||
{t('market.allLoaded')}
|
{searchQuery
|
||||||
</div>
|
? t('market.searchResults', { count: total })
|
||||||
)}
|
: t('market.totalPlugins', { count: total })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 插件详情对话框 */}
|
{/* Scrollable content area */}
|
||||||
|
<div
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
className="flex-1 overflow-y-auto px-3 sm:px-4"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin" />
|
||||||
|
<span className="ml-2">{t('market.loading')}</span>
|
||||||
|
</div>
|
||||||
|
) : plugins.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{searchQuery ? t('market.noResults') : t('market.noPlugins')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6 pb-6">
|
||||||
|
{plugins.map((plugin) => (
|
||||||
|
<PluginMarketCardComponent
|
||||||
|
key={plugin.pluginId}
|
||||||
|
cardVO={plugin}
|
||||||
|
onPluginClick={handlePluginClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading more indicator */}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="flex items-center justify-center py-6">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
<span className="ml-2">{t('market.loadingMore')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No more data hint */}
|
||||||
|
{!hasMore && plugins.length > 0 && (
|
||||||
|
<div className="text-center text-muted-foreground py-6">
|
||||||
|
{t('market.allLoaded')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Plugin detail dialog */}
|
||||||
<PluginDetailDialog
|
<PluginDetailDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onOpenChange={handleDialogClose}
|
onOpenChange={handleDialogClose}
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ export default function MCPComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
{/* 已安装的服务器列表 */}
|
{/* Server list */}
|
||||||
<div className="w-full px-[0.8rem] pt-[0rem] gap-4">
|
<div className="w-full h-full px-[0.8rem] pt-[0rem]">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] w-full gap-2">
|
||||||
{t('mcp.loading')}
|
{t('mcp.loading')}
|
||||||
</div>
|
</div>
|
||||||
) : installedServers.length === 0 ? (
|
) : installedServers.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] w-full gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="h-[3rem] w-[3rem]"
|
className="h-[3rem] w-[3rem]"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -92,7 +92,7 @@ export default function MCPComponent({
|
|||||||
<div className="text-lg mb-2">{t('mcp.noServerInstalled')}</div>
|
<div className="text-lg mb-2">{t('mcp.noServerInstalled')}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pt-[2rem]">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pt-[2rem] pb-6">
|
||||||
{installedServers.map((server, index) => (
|
{installedServers.map((server, index) => (
|
||||||
<div key={`${server.name}-${index}`}>
|
<div key={`${server.name}-${index}`}>
|
||||||
<MCPCardComponent
|
<MCPCardComponent
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ export default function PluginConfigPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
|
className={`${styles.pageContainer} h-full flex flex-col ${isDragOver ? 'bg-blue-50' : ''}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -443,8 +443,12 @@ export default function PluginConfigPage() {
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs
|
||||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="w-full h-full flex flex-col"
|
||||||
|
>
|
||||||
|
<div className="flex flex-row justify-between items-center px-[0.8rem] flex-shrink-0">
|
||||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
||||||
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
||||||
{t('plugins.installed')}
|
{t('plugins.installed')}
|
||||||
@@ -522,10 +526,10 @@ export default function PluginConfigPage() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TabsContent value="installed">
|
<TabsContent value="installed" className="flex-1 overflow-y-auto mt-0">
|
||||||
<PluginInstalledComponent ref={pluginInstalledRef} />
|
<PluginInstalledComponent ref={pluginInstalledRef} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="market">
|
<TabsContent value="market" className="flex-1 overflow-y-auto mt-0">
|
||||||
<MarketPage
|
<MarketPage
|
||||||
installPlugin={(plugin: PluginV4) => {
|
installPlugin={(plugin: PluginV4) => {
|
||||||
setInstallSource('marketplace');
|
setInstallSource('marketplace');
|
||||||
@@ -539,7 +543,10 @@ export default function PluginConfigPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="mcp-servers">
|
<TabsContent
|
||||||
|
value="mcp-servers"
|
||||||
|
className="flex-1 overflow-y-auto mt-0"
|
||||||
|
>
|
||||||
<MCPServerComponent
|
<MCPServerComponent
|
||||||
key={refreshKey}
|
key={refreshKey}
|
||||||
onEditServer={(serverName) => {
|
onEditServer={(serverName) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user