From dd6eff9269c5efc5f995b035e707ad6aad5e5f1b Mon Sep 17 00:00:00 2001 From: xiongxiao Date: Thu, 19 Mar 2026 20:26:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8F=AF=E6=8A=98?= =?UTF-8?q?=E5=8F=A0=E4=BE=A7=E8=BE=B9=E6=A0=8F=E5=B8=83=E5=B1=80=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=93=E5=BA=93=E5=88=97=E8=A1=A8=E5=92=8C?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 +- package.json | 3 + src/components/a/Sidebar.tsx | 270 ++++++++++++++++++ src/pages/config/page.tsx | 10 +- src/pages/gitea/page.tsx | 1 - src/pages/repos/components/RepoCard.tsx | 14 - src/pages/repos/page.tsx | 204 +++++++------ src/pages/repos/repo/page.tsx | 62 ++-- src/pages/sidebar/components/CNBBlackLogo.tsx | 11 + src/pages/sidebar/components/Sidebar.tsx | 35 +++ src/pages/sidebar/components/index.ts | 1 + src/pages/sidebar/page.tsx | 0 src/pages/workspaces/page.tsx | 163 +++++------ 13 files changed, 568 insertions(+), 236 deletions(-) create mode 100644 src/components/a/Sidebar.tsx delete mode 100644 src/pages/gitea/page.tsx create mode 100644 src/pages/sidebar/components/CNBBlackLogo.tsx create mode 100644 src/pages/sidebar/components/Sidebar.tsx create mode 100644 src/pages/sidebar/components/index.ts create mode 100644 src/pages/sidebar/page.tsx diff --git a/README.md b/README.md index 750c655..a2967f0 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,12 @@ # cnb center -> cnb 仓库界面很不好用,所以自己写了一个纯调api的界面,方便管理仓库和查看同步状态 +一个应用工作台 -## 主要功能 +## 功能 -### git仓库管理功能 - -- 创建仓库 -- 删除仓库(使用cookie) -- 同步仓库 - -仓库列出用户所有仓库,主体显示仓库名,描述。每一个仓库具备更快捷的功能模块。 - -启动是启动云开发环境。 - -![intro](./public/images/repo-info.png) - -配置项 - -![config](./public/images/config-info.png) - -示例cookie配置 -```sh -CNBSESSION=1770622649.1935321989751226368.0bc7fc786f7052cb2b077c00ded651a5945d46d1e466f4fafa14ede554da14a0;csrfkey=158893308 -``` \ No newline at end of file +1. 对话管理 +2. 仓库管理 +3. 云开发 +4. 应用管理 +5. Agent管理 +6. 配置 \ No newline at end of file diff --git a/package.json b/package.json index c56bd68..5847e6a 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,11 @@ "lucide-react": "^0.577.0", "nanoid": "^5.1.7", "next-themes": "^0.4.6", + "re-resizable": "^6.11.2", "react": "^19.2.4", "react-dom": "^19.2.4", "react-hook-form": "^7.71.2", + "react-resizable": "^3.1.3", "sonner": "^2.0.7", "vite-plugin-pwa": "^1.2.0", "zod": "^4.3.6", @@ -63,6 +65,7 @@ "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/react-resizable": "^3.0.8", "@vitejs/plugin-react": "^6.0.1", "dotenv": "^17.3.1", "tailwind-merge": "^3.5.0", diff --git a/src/components/a/Sidebar.tsx b/src/components/a/Sidebar.tsx new file mode 100644 index 0000000..239d543 --- /dev/null +++ b/src/components/a/Sidebar.tsx @@ -0,0 +1,270 @@ +'use client' + +import { useNavigate, useLocation } from '@tanstack/react-router' +import { useState } from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' +import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react' +import { Resizable } from 're-resizable' + +export interface NavItem { + title: string + path: string + icon?: React.ReactNode + isDeveloping?: boolean + badge?: string + hidden?: boolean + children?: NavItem[] +} + +export interface SidebarProps { + items: NavItem[] + className?: string + children?: React.ReactNode + logo?: React.ReactNode + title?: React.ReactNode + defaultCollapsed?: boolean + defaultWidth?: number + minWidth?: number + maxWidth?: number +} + +export function Sidebar({ + items, + className, + children, + logo, + title, + defaultCollapsed = false, + defaultWidth = 208, + minWidth = 120, + maxWidth = 400, +}: SidebarProps) { + const navigate = useNavigate() + const location = useLocation() + const currentPath = location.pathname + + const [collapsed, setCollapsed] = useState(defaultCollapsed) + const [sidebarWidth, setSidebarWidth] = useState(defaultWidth) + const [expandedGroups, setExpandedGroups] = useState>(new Set()) + const [developingDialog, setDevelopingDialog] = useState<{ open: boolean; title: string }>({ + open: false, + title: '', + }) + + const toggleGroup = (path: string) => { + const newExpanded = new Set(expandedGroups) + if (newExpanded.has(path)) { + newExpanded.delete(path) + } else { + newExpanded.add(path) + } + setExpandedGroups(newExpanded) + } + + const handleNavClick = (item: NavItem) => { + if (item.isDeveloping) { + setDevelopingDialog({ open: true, title: item.title }) + } else { + navigate({ to: item.path }) + } + } + + // 判断当前路径是否激活(以导航路径开头) + const isActive = (path: string) => { + if (path === '/') { + return currentPath === '/' + } + return currentPath.startsWith(path); + } + + // 渲染导航项 + const renderNavItem = (item: NavItem, isChild = false) => { + if (item.hidden) return null + + const hasChildren = item.children && item.children.length > 0 + const isExpanded = expandedGroups.has(item.path) + const active = isActive(item.path) + + return ( +
  • + {hasChildren ? ( + // 父菜单项(可展开) +
    + + {/* 子菜单 */} + {!collapsed && isExpanded && item.children && ( +
      + {item.children.map(child => renderNavItem(child, true))} +
    + )} +
    + ) : ( + // 普通菜单项 + + )} +
  • + ) + } + + return ( + <> +
    + {/* 侧边栏 */} + {!collapsed ? ( + { + setSidebarWidth(ref.offsetWidth) + }} + enable={{ + right: true, + }} + handleComponent={{ + right:
    , + }} + > + + + ) : ( + // 收起状态 + + )} + + {/* 主内容区域 */} +
    + {children} +
    +
    + + {/* 开发中弹窗 */} + setDevelopingDialog({ open, title: '' })} + > + + + {developingDialog.title} - 开发中 + + 该功能正在紧张开发中,敬请期待!暂时无法访问。 + + + + + + + + + ) +} diff --git a/src/pages/config/page.tsx b/src/pages/config/page.tsx index 6a76ec2..9769cb9 100644 --- a/src/pages/config/page.tsx +++ b/src/pages/config/page.tsx @@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Info } from 'lucide-react'; - +import { SidebarLayout } from '../sidebar/components'; export const ConfigPage = () => { const { config, setConfig, saveToRemote, loadFromRemote } = useConfigStore(); const handleSubmit = (e: React.FormEvent) => { @@ -17,13 +17,13 @@ export const ConfigPage = () => { }; return ( - +

    CNB 配置

    - 配置您的 CNB API 设置。这些设置会保存在浏览器的本地存储中。 + 配置您的 CNB API 设置。

    @@ -68,7 +68,7 @@ export const ConfigPage = () => {

    - 用于身份验证的 Cookie 信息。 + 用于身份验证的 Cookie 信息,有效期7天。 {

    -
    + ); }; diff --git a/src/pages/gitea/page.tsx b/src/pages/gitea/page.tsx deleted file mode 100644 index 5949ba0..0000000 --- a/src/pages/gitea/page.tsx +++ /dev/null @@ -1 +0,0 @@ -// gitea.tsx \ No newline at end of file diff --git a/src/pages/repos/components/RepoCard.tsx b/src/pages/repos/components/RepoCard.tsx index a23fea1..5529d06 100644 --- a/src/pages/repos/components/RepoCard.tsx +++ b/src/pages/repos/components/RepoCard.tsx @@ -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) { 运行中 } - {isMine && ( - { - store.setSelectedSyncRepo(repo) - store.setSyncDialogOpen(true) - }} - > - - 同步 - - )}
    diff --git a/src/pages/repos/page.tsx b/src/pages/repos/page.tsx index cc42f94..4e482b5 100644 --- a/src/pages/repos/page.tsx +++ b/src/pages/repos/page.tsx @@ -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 ( -
    -
    -
    -
    -
    -
    -

    - 仓库列表 - 仓库 - navigate({ to: '/config' })} /> -

    + +
    +
    +
    +
    +
    +
    +

    + 仓库列表 + 仓库 + navigate({ to: '/config' })} /> +

    +
    +

    + {filterDev ? `显示 ${appList.length} 个 dev 仓库` : `共 ${list.length} 个仓库`} +

    -

    共 {list.length} 个仓库

    -
    -
    - - +
    + + - {isCNB && } + {isCNB && } +
    -
    -
    -
    - - setSearchQuery(e.target.value)} - className="pl-10" - /> -
    -
    - -
    - {appList.map((repo) => ( - - ))} -
    - - {appList.length === 0 && !loading && ( -
    - - - - -
    + ) } diff --git a/src/pages/repos/repo/page.tsx b/src/pages/repos/repo/page.tsx index b49016b..2414e7a 100644 --- a/src/pages/repos/repo/page.tsx +++ b/src/pages/repos/repo/page.tsx @@ -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
    Loading...
    } return ( -
    -
    -
    - {tabs.map(tab => ( -
    { - setActiveTab(tab.key) - history.replaceState(null, '', `?repo=${params.repo}&tab=${tab.key}`) - }} - > - {tab.label} -
    - ))} -
    - {activeTab === 'build' && } - {activeTab === 'info' && ( -
    - -
    -
    {JSON.stringify(repoStore.editRepo, null, 2)}
    -
    + +
    +
    +
    + {tabs.map(tab => ( +
    { + setActiveTab(tab.key) + history.replaceState(null, '', `?repo=${params.repo}&tab=${tab.key}`) + }} + > + {tab.label} +
    + ))}
    - )} + {activeTab === 'build' && } + {activeTab === 'info' && ( +
    + +
    +
    {JSON.stringify(repoStore.editRepo, null, 2)}
    +
    +
    + )} +
    +
    - -
    + ) } diff --git a/src/pages/sidebar/components/CNBBlackLogo.tsx b/src/pages/sidebar/components/CNBBlackLogo.tsx new file mode 100644 index 0000000..ea2f17a --- /dev/null +++ b/src/pages/sidebar/components/CNBBlackLogo.tsx @@ -0,0 +1,11 @@ +export const Logo = (props: React.SVGProps) => { + return ( + + + + + + + + ) +} \ No newline at end of file diff --git a/src/pages/sidebar/components/Sidebar.tsx b/src/pages/sidebar/components/Sidebar.tsx new file mode 100644 index 0000000..329843d --- /dev/null +++ b/src/pages/sidebar/components/Sidebar.tsx @@ -0,0 +1,35 @@ +import { FolderKanban, LayoutDashboard, Settings, PlayCircle } from 'lucide-react' +import { Sidebar, type NavItem } from '@/components/a/Sidebar' +import { Logo } from './CNBBlackLogo.tsx' + +const navItems: NavItem[] = [ + { + title: '仓库管理', + path: '/', + icon: , + }, + { + title: '工作空间', + path: '/workspaces', + icon: , + }, + { + title: '应用配置', + path: '/config', + icon: , + }, + { + title: '其他', + path: '/demo', + icon: , + isDeveloping: true, + }, +] + +export function SidebarLayout({ children }: { children: React.ReactNode }) { + return ( + }> + {children} + + ) +} diff --git a/src/pages/sidebar/components/index.ts b/src/pages/sidebar/components/index.ts new file mode 100644 index 0000000..d87c28b --- /dev/null +++ b/src/pages/sidebar/components/index.ts @@ -0,0 +1 @@ +export { SidebarLayout } from './Sidebar' diff --git a/src/pages/sidebar/page.tsx b/src/pages/sidebar/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/workspaces/page.tsx b/src/pages/workspaces/page.tsx index fe8f764..2b6477c 100644 --- a/src/pages/workspaces/page.tsx +++ b/src/pages/workspaces/page.tsx @@ -14,6 +14,7 @@ import { } from "@/components/ui/card"; import { CreateDialog, EditDialog } from "./components"; import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react"; +import { SidebarLayout } from "@/pages/sidebar/components"; export const App = () => { const workspaceStore = useWorkspaceStore(useShallow((state: WorkspaceState) => { @@ -58,92 +59,94 @@ export const App = () => { }; return ( -
    -
    -

    Workspaces

    -
    -
    - - setSearch(e.target.value)} - className="pl-8 w-48" - /> + +
    +
    +

    Workspaces

    +
    +
    + + setSearch(e.target.value)} + className="pl-8 w-48" + /> +
    + +
    - -
    -
    - {workspaceStore.loading ? ( -
    加载中...
    - ) : workspaceStore.list.length === 0 ? ( -
    暂无workspace数据
    - ) : ( -
    - {workspaceStore.list.map((item) => ( - - - {item.title || '未命名'} - ID: {item.id} - - - {item.tags && item.tags.length > 0 && ( -
    - {item.tags.map((tag, index) => ( - {tag} - ))} + {workspaceStore.loading ? ( +
    加载中...
    + ) : workspaceStore.list.length === 0 ? ( +
    暂无workspace数据
    + ) : ( +
    + {workspaceStore.list.map((item) => ( + + + {item.title || '未命名'} + ID: {item.id} + + + {item.tags && item.tags.length > 0 && ( +
    + {item.tags.map((tag, index) => ( + {tag} + ))} +
    + )} + {item.summary && ( +

    {item.summary}

    + )} + {item.description && ( +

    {item.description}

    + )} +

    + 创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'} +

    +

    + 更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'} +

    +
    + +
    - )} - {item.summary && ( -

    {item.summary}

    - )} - {item.description && ( -

    {item.description}

    - )} -

    - 创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'} -

    -

    - 更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'} -

    -
    - - -
    -
    -
    - ))} -
    - )} + + + ))} +
    + )} - workspaceStore.setShowCreateDialog(false)} - onSubmit={workspaceStore.createItem} - /> + workspaceStore.setShowCreateDialog(false)} + onSubmit={workspaceStore.createItem} + /> - { - workspaceStore.setShowEditDialog(false); - workspaceStore.setEditingItem(null); - }} - onSubmit={workspaceStore.updateItem} - /> -
    + { + workspaceStore.setShowEditDialog(false); + workspaceStore.setEditingItem(null); + }} + onSubmit={workspaceStore.updateItem} + /> +
    + ); };