generated from kevisual/vite-react-template
feat: 添加可折叠侧边栏布局,优化仓库列表和工作空间页面
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user