feat: 添加可折叠侧边栏布局,优化仓库列表和工作空间页面

This commit is contained in:
xiongxiao
2026-03-19 20:26:27 +08:00
committed by cnb
parent 9a06364880
commit dd6eff9269
13 changed files with 568 additions and 236 deletions

View File

@@ -47,8 +47,6 @@ export function RepoCard({ showReturn = false, repo }: RepoCardProps) {
return store.workspaceList.find(ws => ws.slug === repo.path)
}, [store.workspaceList, repo.path])
const isWorkspaceActive = !!workspace
const owner = repo.path.split('/')[0]
const isMine = myOrgs.includes(owner)
const navigate = useNavigate();
const isKnowledge = repo?.flags === "KnowledgeBase"
const createKnow = async () => {
@@ -346,18 +344,6 @@ export function RepoCard({ showReturn = false, repo }: RepoCardProps) {
<Play className="w-3.5 h-3.5" />
<span className="font-medium"></span>
</span>}
{isMine && (
<span
className="flex items-center gap-1 hover:text-neutral-900 transition-colors cursor-pointer whitespace-nowrap"
onClick={() => {
store.setSelectedSyncRepo(repo)
store.setSyncDialogOpen(true)
}}
>
<RefreshCw className="w-3.5 h-3.5" />
<span className="font-medium"></span>
</span>
)}
</div>
</div>
</Card>

View File

@@ -12,6 +12,8 @@ import Fuse from 'fuse.js'
import { useNavigate } from '@tanstack/react-router'
import { useLayoutStore } from '../auth/store'
import { useConfigStore } from '../config/store'
import { SidebarLayout } from '../sidebar/components'
import { Checkbox } from '@/components/ui/checkbox'
export const App = () => {
const { list, refresh, loading, workspaceList, setShowCreateDialog } = useRepoStore(useShallow((state) => ({
@@ -22,6 +24,7 @@ export const App = () => {
setShowCreateDialog: state.setShowCreateDialog,
})))
const [searchQuery, setSearchQuery] = useState('')
const [filterDev, setFilterDev] = useState(false)
const navigate = useNavigate();
const me = useLayoutStore(state => state.me)
const configStore = useConfigStore(useShallow(state => ({ checkConfig: state.checkConfig })))
@@ -45,11 +48,19 @@ export const App = () => {
return 0
})
if (!searchQuery.trim()) {
return sortedList
let filteredList = sortedList
if (filterDev) {
filteredList = sortedList.filter(repo => {
const topics = repo.topics ? repo.topics.split(',').map(t => t.trim().toLowerCase()) : []
return topics.some(topic => topic.includes('dev'))
})
}
const fuse = new Fuse(sortedList, {
if (!searchQuery.trim()) {
return filteredList
}
const fuse = new Fuse(filteredList, {
keys: ['name', 'path', 'description'],
threshold: 0.3,
includeScore: true
@@ -57,101 +68,120 @@ export const App = () => {
const results = fuse.search(searchQuery)
return results.map(result => result.item)
}, [list, workspaceList, searchQuery])
}, [list, workspaceList, searchQuery, filterDev])
const isCNB = location.hostname.includes('cnb.run')
return (
<div className="min-h-screen bg-neutral-50 flex flex-col">
<div className="container mx-auto p-4 md:p-6 max-w-7xl flex-1">
<div className="mb-6 md:mb-8 flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:justify-between gap-3">
<div className=''>
<div className="flex items-center justify-between">
<h1 className="text-2xl md:text-4xl font-bold text-neutral-900 flex gap-2 items-center">
<span className="hidden md:inline"></span>
<span className="md:hidden"></span>
<Settings className="inline-block h-5 w-5 text-neutral-400 hover:text-neutral-600 cursor-pointer" onClick={() => navigate({ to: '/config' })} />
</h1>
<SidebarLayout>
<div className="min-h-screen bg-neutral-50 flex flex-col">
<div className="container mx-auto p-4 md:p-6 max-w-7xl flex-1">
<div className="mb-6 md:mb-8 flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:justify-between gap-3">
<div className=''>
<div className="flex items-center justify-between">
<h1 className="text-2xl md:text-4xl font-bold text-neutral-900 flex gap-2 items-center">
<span className="hidden md:inline"></span>
<span className="md:hidden"></span>
<Settings className="inline-block h-5 w-5 text-neutral-400 hover:text-neutral-600 cursor-pointer" onClick={() => navigate({ to: '/config' })} />
</h1>
</div>
<p className="text-neutral-600 text-sm md:text-base">
{filterDev ? `显示 ${appList.length} 个 dev 仓库` : `${list.length} 个仓库`}
</p>
</div>
<p className="text-neutral-600 text-sm md:text-base"> {list.length} </p>
</div>
<div className="flex flex-wrap items-center gap-2 md:ml-auto">
<Button
onClick={() => refresh()}
variant="outline"
className="gap-2 flex-1 sm:flex-none"
>
<RefreshCw className="h-4 w-4" />
<span className="hidden sm:inline"></span>
</Button>
<Button
onClick={() => setShowCreateDialog(true)}
className="gap-2 flex-1 sm:flex-none"
>
<Plus className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</Button>
<div className="flex flex-wrap items-center gap-2 md:ml-auto">
<Button
onClick={() => refresh()}
variant="outline"
className="gap-2 flex-1 sm:flex-none"
>
<RefreshCw className="h-4 w-4" />
<span className="hidden sm:inline"></span>
</Button>
<Button
onClick={() => setShowCreateDialog(true)}
className="gap-2 flex-1 sm:flex-none"
>
<Plus className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</Button>
{isCNB && <Button
onClick={() => {
window.open('/root/cli-center', '_blank')
}}
className="gap-2 hidden md:flex"
>
<ExternalLinkIcon className="h-4 w-4" />
CLI
</Button>}
{isCNB && <Button
onClick={() => {
window.open('/root/cli-center', '_blank')
}}
className="gap-2 hidden md:flex"
>
<ExternalLinkIcon className="h-4 w-4" />
CLI
</Button>}
</div>
</div>
</div>
</div>
<div className="mb-4 md:mb-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-400" />
<Input
type="text"
placeholder="搜索仓库..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
{appList.map((repo) => (
<RepoCard
key={repo.id}
repo={repo}
/>
))}
</div>
{appList.length === 0 && !loading && (
<div className="text-center py-20">
<div className="text-neutral-400 text-lg">
{searchQuery ? '未找到匹配的仓库' : '暂无仓库数据'}
<div className="mb-4 md:mb-6">
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-400" />
<Input
type="text"
placeholder="搜索仓库..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="filter-dev"
checked={filterDev}
onCheckedChange={(checked) => setFilterDev(checked === true)}
/>
<label
htmlFor="filter-dev"
className="text-sm text-neutral-600 cursor-pointer select-none"
>
dev
</label>
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
{appList.map((repo) => (
<RepoCard
key={repo.id}
repo={repo}
/>
))}
</div>
{appList.length === 0 && !loading && (
<div className="text-center py-20">
<div className="text-neutral-400 text-lg">
{searchQuery ? '未找到匹配的仓库' : '暂无仓库数据'}
</div>
</div>
)}
</div>
<footer className="border-t border-neutral-200 bg-white py-4 md:py-6 mt-auto">
<div className="container mx-auto px-4 md:px-6 max-w-7xl">
<div className="flex flex-col md:flex-row items-center justify-between gap-2 md:gap-4 text-sm text-neutral-500">
<div>© 2026 </div>
<div className="flex items-center gap-4">
<a href="#" className="hover:text-neutral-900 transition-colors"></a>
<a href="#" className="hover:text-neutral-900 transition-colors"></a>
<a href="#" className="hover:text-neutral-900 transition-colors"></a>
</div>
</div>
</div>
</footer>
<CommonRepoDialog />
</div>
<footer className="border-t border-neutral-200 bg-white py-4 md:py-6 mt-auto">
<div className="container mx-auto px-4 md:px-6 max-w-7xl">
<div className="flex flex-col md:flex-row items-center justify-between gap-2 md:gap-4 text-sm text-neutral-500">
<div>© 2026 </div>
<div className="flex items-center gap-4">
<a href="#" className="hover:text-neutral-900 transition-colors"></a>
<a href="#" className="hover:text-neutral-900 transition-colors"></a>
<a href="#" className="hover:text-neutral-900 transition-colors"></a>
</div>
</div>
</div>
</footer>
<CommonRepoDialog />
</div>
</SidebarLayout>
)
}

View File

@@ -5,6 +5,7 @@ import { useShallow } from "zustand/shallow";
import BuildConfig from "../components/BuildConfig";
import { CommonRepoDialog } from "../page";
import { RepoCard } from "../components/RepoCard";
import { SidebarLayout } from "@/pages/sidebar/components";
export const App = () => {
const params = useSearch({ strict: false }) as { repo?: string, tab?: string };
@@ -13,6 +14,7 @@ export const App = () => {
editRepo: state.editRepo,
refresh: state.refresh,
loading: state.loading,
buildConfig: state.buildConfig,
})));
const [activeTab, setActiveTab] = useState(params.tab || "build");
const tabs = [
@@ -21,7 +23,11 @@ export const App = () => {
]
useEffect(() => {
if (params.repo) {
repoStore.getItem(params.repo);
if(repoStore.buildConfig?.repo !== params.repo) {
repoStore.getItem(params.repo);
}
console.log('refreshing repo',repoStore.buildConfig, params.repo)
// repoStore.getItem(params.repo);
repoStore.refresh({ search: params.repo, showTips: false });
} else {
console.log('no repo param')
@@ -31,34 +37,36 @@ export const App = () => {
return <div>Loading...</div>
}
return (
<div className="p-2 md:p-4 flex-col flex gap-2 md:gap-4 h-full">
<div className="px-2 md:px-4 h-full scrollbar flex-col flex gap-3 md:gap-4 overflow-hidden">
<div className="flex border-b overflow-x-auto">
{tabs.map(tab => (
<div
key={tab.key}
className={`px-3 md:px-4 py-2 cursor-pointer whitespace-nowrap text-sm md:text-base ${activeTab === tab.key ? 'border-b-2 border-gray-500 font-medium' : ''}`}
onClick={() => {
setActiveTab(tab.key)
history.replaceState(null, '', `?repo=${params.repo}&tab=${tab.key}`)
}}
>
{tab.label}
</div>
))}
</div>
{activeTab === 'build' && <BuildConfig />}
{activeTab === 'info' && (
<div className="flex flex-col gap-3 md:gap-4 h-full">
<RepoCard repo={repoStore.editRepo} showReturn />
<div className="p-3 md:p-4 border rounded bg-white h-full overflow-auto scrollbar">
<pre className="whitespace-pre-wrap break-all text-xs md:text-sm">{JSON.stringify(repoStore.editRepo, null, 2)}</pre>
</div>
<SidebarLayout>
<div className="p-2 md:p-4 flex-col flex gap-2 md:gap-4 h-full">
<div className="px-2 md:px-4 h-full scrollbar flex-col flex gap-3 md:gap-4 overflow-hidden">
<div className="flex border-b overflow-x-auto h-12 shrink-0">
{tabs.map(tab => (
<div
key={tab.key}
className={`px-3 md:px-4 py-2 cursor-pointer whitespace-nowrap text-sm md:text-base ${activeTab === tab.key ? 'border-b-2 border-gray-500 font-medium' : ''}`}
onClick={() => {
setActiveTab(tab.key)
history.replaceState(null, '', `?repo=${params.repo}&tab=${tab.key}`)
}}
>
{tab.label}
</div>
))}
</div>
)}
{activeTab === 'build' && <BuildConfig />}
{activeTab === 'info' && (
<div className="flex flex-col gap-3 md:gap-4 h-full">
<RepoCard repo={repoStore.editRepo} showReturn />
<div className="p-3 md:p-4 border rounded bg-white h-full overflow-auto scrollbar">
<pre className="whitespace-pre-wrap break-all text-xs md:text-sm">{JSON.stringify(repoStore.editRepo, null, 2)}</pre>
</div>
</div>
)}
</div>
<CommonRepoDialog />
</div>
<CommonRepoDialog />
</div>
</SidebarLayout>
)
}