generated from kevisual/vite-react-template
feat: 添加可折叠侧边栏布局,优化仓库列表和工作空间页面
This commit is contained in:
30
README.md
30
README.md
@@ -1,26 +1,12 @@
|
|||||||
# cnb center
|
# cnb center
|
||||||
|
|
||||||
> cnb 仓库界面很不好用,所以自己写了一个纯调api的界面,方便管理仓库和查看同步状态
|
一个应用工作台
|
||||||
|
|
||||||
## 主要功能
|
## 功能
|
||||||
|
|
||||||
### git仓库管理功能
|
1. 对话管理
|
||||||
|
2. 仓库管理
|
||||||
- 创建仓库
|
3. 云开发
|
||||||
- 删除仓库(使用cookie)
|
4. 应用管理
|
||||||
- 同步仓库
|
5. Agent管理
|
||||||
|
6. 配置
|
||||||
仓库列出用户所有仓库,主体显示仓库名,描述。每一个仓库具备更快捷的功能模块。
|
|
||||||
|
|
||||||
启动是启动云开发环境。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
配置项
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
示例cookie配置
|
|
||||||
```sh
|
|
||||||
CNBSESSION=1770622649.1935321989751226368.0bc7fc786f7052cb2b077c00ded651a5945d46d1e466f4fafa14ede554da14a0;csrfkey=158893308
|
|
||||||
```
|
|
||||||
@@ -40,9 +40,11 @@
|
|||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"nanoid": "^5.1.7",
|
"nanoid": "^5.1.7",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"re-resizable": "^6.11.2",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.71.2",
|
"react-hook-form": "^7.71.2",
|
||||||
|
"react-resizable": "^3.1.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/react-resizable": "^3.0.8",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
|||||||
270
src/components/a/Sidebar.tsx
Normal file
270
src/components/a/Sidebar.tsx
Normal file
@@ -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<Set<string>>(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 (
|
||||||
|
<li key={item.path} className='list-none'>
|
||||||
|
{hasChildren ? (
|
||||||
|
// 父菜单项(可展开)
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleGroup(item.path)}
|
||||||
|
className={cn(
|
||||||
|
'w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors cursor-pointer',
|
||||||
|
active
|
||||||
|
? 'bg-gray-200 text-gray-900 font-medium'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
|
||||||
|
collapsed && 'justify-center px-2'
|
||||||
|
)}
|
||||||
|
title={collapsed ? item.title : undefined}
|
||||||
|
>
|
||||||
|
{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
|
||||||
|
{!collapsed && (
|
||||||
|
<>
|
||||||
|
<span className="flex-1 text-left">{item.title}</span>
|
||||||
|
<ChevronDown className={cn('w-4 h-4 transition-transform', isExpanded && 'rotate-180')} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{/* 子菜单 */}
|
||||||
|
{!collapsed && isExpanded && item.children && (
|
||||||
|
<ul className="ml-6 mt-1 space-y-1 list-none">
|
||||||
|
{item.children.map(child => renderNavItem(child, true))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 普通菜单项
|
||||||
|
<button
|
||||||
|
onClick={() => handleNavClick(item)}
|
||||||
|
className={cn(
|
||||||
|
'w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors cursor-pointer',
|
||||||
|
active
|
||||||
|
? 'bg-gray-200 text-gray-900 font-medium'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
|
||||||
|
item.isDeveloping && 'opacity-60',
|
||||||
|
isChild && 'py-1.5',
|
||||||
|
collapsed && 'justify-center px-2'
|
||||||
|
)}
|
||||||
|
title={collapsed ? item.title : undefined}
|
||||||
|
>
|
||||||
|
{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
|
||||||
|
{!collapsed && (
|
||||||
|
<>
|
||||||
|
<span className="flex-1 text-left">{item.title}</span>
|
||||||
|
{item.isDeveloping && (
|
||||||
|
<span className="text-xs bg-gray-200 text-gray-600 px-1.5 py-0.5 rounded">
|
||||||
|
dev
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.badge && !item.isDeveloping && (
|
||||||
|
<span className="text-xs bg-gray-200 text-gray-700 px-1.5 py-0.5 rounded">
|
||||||
|
{item.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cn('flex h-full', className)}>
|
||||||
|
{/* 侧边栏 */}
|
||||||
|
{!collapsed ? (
|
||||||
|
<Resizable
|
||||||
|
defaultSize={{ width: sidebarWidth, height: '100%' }}
|
||||||
|
minWidth={minWidth}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
onResizeStop={(_e, _direction, ref, _d) => {
|
||||||
|
setSidebarWidth(ref.offsetWidth)
|
||||||
|
}}
|
||||||
|
enable={{
|
||||||
|
right: true,
|
||||||
|
}}
|
||||||
|
handleComponent={{
|
||||||
|
right: <div className="w-1 h-full cursor-col-resize hover:bg-blue-400 transition-colors" />,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
'border-r bg-white flex-shrink-0 flex flex-col'
|
||||||
|
)}
|
||||||
|
style={{ width: sidebarWidth }}
|
||||||
|
>
|
||||||
|
{/* Logo 区域 */}
|
||||||
|
<div className={cn(
|
||||||
|
'h-12 flex items-center border-b px-3'
|
||||||
|
)}>
|
||||||
|
{logo && (
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-2">{logo}</div>
|
||||||
|
)}
|
||||||
|
{title && (
|
||||||
|
<span className="text-sm font-medium text-gray-900 truncate ml-1">{title}</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setCollapsed(true)}
|
||||||
|
className="ml-auto flex-shrink-0 p-1 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
|
||||||
|
title="收起"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 导航区域 */}
|
||||||
|
<nav className="flex-1 p-2 overflow-y-auto">
|
||||||
|
<ul className="space-y-1 list-none">
|
||||||
|
{items.map((item) => renderNavItem(item))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
</Resizable>
|
||||||
|
) : (
|
||||||
|
// 收起状态
|
||||||
|
<aside className="w-14 border-r bg-white flex-shrink-0 flex flex-col">
|
||||||
|
{/* Logo 区域 */}
|
||||||
|
<div className="h-12 flex items-center justify-center border-b px-2">
|
||||||
|
<div className="group relative flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setCollapsed(false)}
|
||||||
|
className="p-1 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
|
||||||
|
title="展开"
|
||||||
|
>
|
||||||
|
<span className="group-hover:hidden">{logo}</span>
|
||||||
|
<ChevronRight className="w-5 h-5 hidden group-hover:block text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 导航区域 */}
|
||||||
|
<nav className="flex-1 p-2 overflow-y-auto">
|
||||||
|
<ul className="space-y-1 list-none">
|
||||||
|
{items.map((item) => renderNavItem(item))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 主内容区域 */}
|
||||||
|
<main className="flex-1 overflow-auto h-full bg-gray-50">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 开发中弹窗 */}
|
||||||
|
<Dialog
|
||||||
|
open={developingDialog.open}
|
||||||
|
onOpenChange={(open) => setDevelopingDialog({ open, title: '' })}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{developingDialog.title} - 开发中</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
该功能正在紧张开发中,敬请期待!暂时无法访问。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={() => setDevelopingDialog({ open: false, title: '' })}>
|
||||||
|
知道了
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
import { SidebarLayout } from '../sidebar/components';
|
||||||
export const ConfigPage = () => {
|
export const ConfigPage = () => {
|
||||||
const { config, setConfig, saveToRemote, loadFromRemote } = useConfigStore();
|
const { config, setConfig, saveToRemote, loadFromRemote } = useConfigStore();
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
@@ -17,13 +17,13 @@ export const ConfigPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<SidebarLayout>
|
||||||
<div className="container mx-auto max-w-2xl py-4 md:py-8 px-4">
|
<div className="container mx-auto max-w-2xl py-4 md:py-8 px-4">
|
||||||
<div className="bg-white md:rounded-lg md:border md:shadow-sm">
|
<div className="bg-white md:rounded-lg md:border md:shadow-sm">
|
||||||
<div className="p-4 md:p-6 border-b">
|
<div className="p-4 md:p-6 border-b">
|
||||||
<h1 className="text-xl md:text-2xl font-bold">CNB 配置</h1>
|
<h1 className="text-xl md:text-2xl font-bold">CNB 配置</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
配置您的 CNB API 设置。这些设置会保存在浏览器的本地存储中。
|
配置您的 CNB API 设置。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 md:p-6">
|
<div className="p-4 md:p-6">
|
||||||
@@ -68,7 +68,7 @@ export const ConfigPage = () => {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
用于身份验证的 Cookie 信息。
|
用于身份验证的 Cookie 信息,有效期7天。
|
||||||
<a
|
<a
|
||||||
href="https://cnb.cool/kevisual/cnb-live-extension"
|
href="https://cnb.cool/kevisual/cnb-live-extension"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -102,7 +102,7 @@ export const ConfigPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</SidebarLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
// gitea.tsx
|
|
||||||
@@ -47,8 +47,6 @@ export function RepoCard({ showReturn = false, repo }: RepoCardProps) {
|
|||||||
return store.workspaceList.find(ws => ws.slug === repo.path)
|
return store.workspaceList.find(ws => ws.slug === repo.path)
|
||||||
}, [store.workspaceList, repo.path])
|
}, [store.workspaceList, repo.path])
|
||||||
const isWorkspaceActive = !!workspace
|
const isWorkspaceActive = !!workspace
|
||||||
const owner = repo.path.split('/')[0]
|
|
||||||
const isMine = myOrgs.includes(owner)
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isKnowledge = repo?.flags === "KnowledgeBase"
|
const isKnowledge = repo?.flags === "KnowledgeBase"
|
||||||
const createKnow = async () => {
|
const createKnow = async () => {
|
||||||
@@ -346,18 +344,6 @@ export function RepoCard({ showReturn = false, repo }: RepoCardProps) {
|
|||||||
<Play className="w-3.5 h-3.5" />
|
<Play className="w-3.5 h-3.5" />
|
||||||
<span className="font-medium">运行中</span>
|
<span className="font-medium">运行中</span>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import Fuse from 'fuse.js'
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
import { useLayoutStore } from '../auth/store'
|
import { useLayoutStore } from '../auth/store'
|
||||||
import { useConfigStore } from '../config/store'
|
import { useConfigStore } from '../config/store'
|
||||||
|
import { SidebarLayout } from '../sidebar/components'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const { list, refresh, loading, workspaceList, setShowCreateDialog } = useRepoStore(useShallow((state) => ({
|
const { list, refresh, loading, workspaceList, setShowCreateDialog } = useRepoStore(useShallow((state) => ({
|
||||||
@@ -22,6 +24,7 @@ export const App = () => {
|
|||||||
setShowCreateDialog: state.setShowCreateDialog,
|
setShowCreateDialog: state.setShowCreateDialog,
|
||||||
})))
|
})))
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [filterDev, setFilterDev] = useState(false)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const me = useLayoutStore(state => state.me)
|
const me = useLayoutStore(state => state.me)
|
||||||
const configStore = useConfigStore(useShallow(state => ({ checkConfig: state.checkConfig })))
|
const configStore = useConfigStore(useShallow(state => ({ checkConfig: state.checkConfig })))
|
||||||
@@ -45,11 +48,19 @@ export const App = () => {
|
|||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!searchQuery.trim()) {
|
let filteredList = sortedList
|
||||||
return 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'],
|
keys: ['name', 'path', 'description'],
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
includeScore: true
|
includeScore: true
|
||||||
@@ -57,101 +68,120 @@ export const App = () => {
|
|||||||
|
|
||||||
const results = fuse.search(searchQuery)
|
const results = fuse.search(searchQuery)
|
||||||
return results.map(result => result.item)
|
return results.map(result => result.item)
|
||||||
}, [list, workspaceList, searchQuery])
|
}, [list, workspaceList, searchQuery, filterDev])
|
||||||
|
|
||||||
const isCNB = location.hostname.includes('cnb.run')
|
const isCNB = location.hostname.includes('cnb.run')
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neutral-50 flex flex-col">
|
<SidebarLayout>
|
||||||
<div className="container mx-auto p-4 md:p-6 max-w-7xl flex-1">
|
<div className="min-h-screen bg-neutral-50 flex flex-col">
|
||||||
<div className="mb-6 md:mb-8 flex flex-col gap-4">
|
<div className="container mx-auto p-4 md:p-6 max-w-7xl flex-1">
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-3">
|
<div className="mb-6 md:mb-8 flex flex-col gap-4">
|
||||||
<div className=''>
|
<div className="flex flex-col sm:flex-row sm:justify-between gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className=''>
|
||||||
<h1 className="text-2xl md:text-4xl font-bold text-neutral-900 flex gap-2 items-center">
|
<div className="flex items-center justify-between">
|
||||||
<span className="hidden md:inline">仓库列表</span>
|
<h1 className="text-2xl md:text-4xl font-bold text-neutral-900 flex gap-2 items-center">
|
||||||
<span className="md:hidden">仓库</span>
|
<span className="hidden md:inline">仓库列表</span>
|
||||||
<Settings className="inline-block h-5 w-5 text-neutral-400 hover:text-neutral-600 cursor-pointer" onClick={() => navigate({ to: '/config' })} />
|
<span className="md:hidden">仓库</span>
|
||||||
</h1>
|
<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>
|
</div>
|
||||||
<p className="text-neutral-600 text-sm md:text-base">共 {list.length} 个仓库</p>
|
<div className="flex flex-wrap items-center gap-2 md:ml-auto">
|
||||||
</div>
|
<Button
|
||||||
<div className="flex flex-wrap items-center gap-2 md:ml-auto">
|
onClick={() => refresh()}
|
||||||
<Button
|
variant="outline"
|
||||||
onClick={() => refresh()}
|
className="gap-2 flex-1 sm:flex-none"
|
||||||
variant="outline"
|
>
|
||||||
className="gap-2 flex-1 sm:flex-none"
|
<RefreshCw className="h-4 w-4" />
|
||||||
>
|
<span className="hidden sm:inline">刷新</span>
|
||||||
<RefreshCw className="h-4 w-4" />
|
</Button>
|
||||||
<span className="hidden sm:inline">刷新</span>
|
<Button
|
||||||
</Button>
|
onClick={() => setShowCreateDialog(true)}
|
||||||
<Button
|
className="gap-2 flex-1 sm:flex-none"
|
||||||
onClick={() => setShowCreateDialog(true)}
|
>
|
||||||
className="gap-2 flex-1 sm:flex-none"
|
<Plus className="h-4 w-4" />
|
||||||
>
|
<span className="hidden sm:inline">新建仓库</span>
|
||||||
<Plus className="h-4 w-4" />
|
<span className="sm:hidden">新建</span>
|
||||||
<span className="hidden sm:inline">新建仓库</span>
|
</Button>
|
||||||
<span className="sm:hidden">新建</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isCNB && <Button
|
{isCNB && <Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open('/root/cli-center', '_blank')
|
window.open('/root/cli-center', '_blank')
|
||||||
}}
|
}}
|
||||||
className="gap-2 hidden md:flex"
|
className="gap-2 hidden md:flex"
|
||||||
>
|
>
|
||||||
<ExternalLinkIcon className="h-4 w-4" />
|
<ExternalLinkIcon className="h-4 w-4" />
|
||||||
CLI
|
CLI
|
||||||
</Button>}
|
</Button>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4 md:mb-6">
|
<div className="mb-4 md:mb-6">
|
||||||
<div className="relative">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
<div className="relative flex-1">
|
||||||
<Input
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||||
type="text"
|
<Input
|
||||||
placeholder="搜索仓库..."
|
type="text"
|
||||||
value={searchQuery}
|
placeholder="搜索仓库..."
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
value={searchQuery}
|
||||||
className="pl-10"
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
className="pl-10"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
<Checkbox
|
||||||
{appList.map((repo) => (
|
id="filter-dev"
|
||||||
<RepoCard
|
checked={filterDev}
|
||||||
key={repo.id}
|
onCheckedChange={(checked) => setFilterDev(checked === true)}
|
||||||
repo={repo}
|
/>
|
||||||
/>
|
<label
|
||||||
))}
|
htmlFor="filter-dev"
|
||||||
</div>
|
className="text-sm text-neutral-600 cursor-pointer select-none"
|
||||||
|
>
|
||||||
{appList.length === 0 && !loading && (
|
过滤 dev
|
||||||
<div className="text-center py-20">
|
</label>
|
||||||
<div className="text-neutral-400 text-lg">
|
</div>
|
||||||
{searchQuery ? '未找到匹配的仓库' : '暂无仓库数据'}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
</SidebarLayout>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useShallow } from "zustand/shallow";
|
|||||||
import BuildConfig from "../components/BuildConfig";
|
import BuildConfig from "../components/BuildConfig";
|
||||||
import { CommonRepoDialog } from "../page";
|
import { CommonRepoDialog } from "../page";
|
||||||
import { RepoCard } from "../components/RepoCard";
|
import { RepoCard } from "../components/RepoCard";
|
||||||
|
import { SidebarLayout } from "@/pages/sidebar/components";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const params = useSearch({ strict: false }) as { repo?: string, tab?: string };
|
const params = useSearch({ strict: false }) as { repo?: string, tab?: string };
|
||||||
@@ -13,6 +14,7 @@ export const App = () => {
|
|||||||
editRepo: state.editRepo,
|
editRepo: state.editRepo,
|
||||||
refresh: state.refresh,
|
refresh: state.refresh,
|
||||||
loading: state.loading,
|
loading: state.loading,
|
||||||
|
buildConfig: state.buildConfig,
|
||||||
})));
|
})));
|
||||||
const [activeTab, setActiveTab] = useState(params.tab || "build");
|
const [activeTab, setActiveTab] = useState(params.tab || "build");
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -21,7 +23,11 @@ export const App = () => {
|
|||||||
]
|
]
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (params.repo) {
|
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 });
|
repoStore.refresh({ search: params.repo, showTips: false });
|
||||||
} else {
|
} else {
|
||||||
console.log('no repo param')
|
console.log('no repo param')
|
||||||
@@ -31,34 +37,36 @@ export const App = () => {
|
|||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="p-2 md:p-4 flex-col flex gap-2 md:gap-4 h-full">
|
<SidebarLayout>
|
||||||
<div className="px-2 md:px-4 h-full scrollbar flex-col flex gap-3 md:gap-4 overflow-hidden">
|
<div className="p-2 md:p-4 flex-col flex gap-2 md:gap-4 h-full">
|
||||||
<div className="flex border-b overflow-x-auto">
|
<div className="px-2 md:px-4 h-full scrollbar flex-col flex gap-3 md:gap-4 overflow-hidden">
|
||||||
{tabs.map(tab => (
|
<div className="flex border-b overflow-x-auto h-12 shrink-0">
|
||||||
<div
|
{tabs.map(tab => (
|
||||||
key={tab.key}
|
<div
|
||||||
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' : ''}`}
|
key={tab.key}
|
||||||
onClick={() => {
|
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' : ''}`}
|
||||||
setActiveTab(tab.key)
|
onClick={() => {
|
||||||
history.replaceState(null, '', `?repo=${params.repo}&tab=${tab.key}`)
|
setActiveTab(tab.key)
|
||||||
}}
|
history.replaceState(null, '', `?repo=${params.repo}&tab=${tab.key}`)
|
||||||
>
|
}}
|
||||||
{tab.label}
|
>
|
||||||
</div>
|
{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>
|
||||||
)}
|
{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>
|
</div>
|
||||||
<CommonRepoDialog />
|
</SidebarLayout>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
src/pages/sidebar/components/CNBBlackLogo.tsx
Normal file
11
src/pages/sidebar/components/CNBBlackLogo.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export const Logo = (props: React.SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg {...props} width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M228.906 40.2412C229.882 37.5108 228.906 34.3903 226.759 32.44C219.342 26.004 200.799 12.3519 173.082 10.4016C141.852 8.06121 122.528 16.4475 112.769 22.6885C108.474 25.4189 108.279 31.4649 112.183 34.3903L191.625 96.2149C198.652 101.676 208.997 98.5553 211.729 90.169L228.711 40.2412H228.906Z" fill="black" />
|
||||||
|
<path d="M32.9381 223.564C29.6199 225.71 28.2536 229.805 29.2295 233.511C32.1573 244.432 41.3312 266.861 66.9009 287.534C92.4706 308.012 122.725 310.353 135.607 309.963C139.511 309.963 142.829 307.427 144 303.722L194.945 142.627C198.653 130.925 185.576 121.173 175.426 127.999L32.9381 223.564Z" fill="black" />
|
||||||
|
<path d="M70.2169 53.4955C67.6794 52.5203 64.9468 52.7153 62.6045 53.8855C53.2355 58.9563 29.032 74.7538 16.54 107.324C6.78054 132.288 10.0987 159.982 12.8314 173.439C13.6121 177.925 18.2967 180.46 22.5908 178.705L175.424 119.026C186.354 114.735 186.354 99.3276 175.424 95.0369L70.2169 53.4955Z" fill="black" />
|
||||||
|
<path d="M297.03 168.968C301.519 171.893 307.57 169.358 308.351 164.092C310.303 150.05 312.06 125.866 304.057 107.338C293.321 82.9591 274.974 67.7468 266.19 61.7008C263.458 59.7505 259.749 59.9456 257.212 62.2859L218.564 96.4162C212.318 102.072 212.904 112.019 219.931 116.699L297.03 168.968Z" fill="black" />
|
||||||
|
<path d="M189.089 299.428C188.699 303.914 192.603 307.814 197.092 307.229C211.731 305.669 241.79 299.818 264.237 278.365C286.098 257.496 293.32 232.728 295.272 222.781C295.858 220.051 295.272 217.32 293.515 215.175L225.98 131.897C218.758 122.925 204.119 127.411 203.143 138.918L189.089 299.233V299.428Z" fill="black" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
src/pages/sidebar/components/Sidebar.tsx
Normal file
35
src/pages/sidebar/components/Sidebar.tsx
Normal file
@@ -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: <FolderKanban className="w-5 h-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '工作空间',
|
||||||
|
path: '/workspaces',
|
||||||
|
icon: <LayoutDashboard className="w-5 h-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '应用配置',
|
||||||
|
path: '/config',
|
||||||
|
icon: <Settings className="w-5 h-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '其他',
|
||||||
|
path: '/demo',
|
||||||
|
icon: <PlayCircle className="w-5 h-5" />,
|
||||||
|
isDeveloping: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function SidebarLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Sidebar items={navItems} title='云原生' logo={<Logo className='w-6 h-6' />}>
|
||||||
|
{children}
|
||||||
|
</Sidebar>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
src/pages/sidebar/components/index.ts
Normal file
1
src/pages/sidebar/components/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { SidebarLayout } from './Sidebar'
|
||||||
0
src/pages/sidebar/page.tsx
Normal file
0
src/pages/sidebar/page.tsx
Normal file
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { CreateDialog, EditDialog } from "./components";
|
import { CreateDialog, EditDialog } from "./components";
|
||||||
import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react";
|
import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||||
|
import { SidebarLayout } from "@/pages/sidebar/components";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const workspaceStore = useWorkspaceStore(useShallow((state: WorkspaceState) => {
|
const workspaceStore = useWorkspaceStore(useShallow((state: WorkspaceState) => {
|
||||||
@@ -58,92 +59,94 @@ export const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-5">
|
<SidebarLayout>
|
||||||
<div className="flex justify-between items-center mb-5">
|
<div className="p-5">
|
||||||
<h1 className="text-2xl font-semibold">Workspaces</h1>
|
<div className="flex justify-between items-center mb-5">
|
||||||
<div className="flex gap-2">
|
<h1 className="text-2xl font-semibold">Workspaces</h1>
|
||||||
<div className="relative">
|
<div className="flex gap-2">
|
||||||
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
<div className="relative">
|
||||||
<Input
|
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
type="text"
|
<Input
|
||||||
placeholder="搜索workspace..."
|
type="text"
|
||||||
value={search}
|
placeholder="搜索workspace..."
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
value={search}
|
||||||
className="pl-8 w-48"
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
className="pl-8 w-48"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||||
|
<RefreshCwIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleCreate}>
|
||||||
|
<PlusIcon className="size-4" />
|
||||||
|
创建Workspace
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
|
||||||
<RefreshCwIcon className="size-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={handleCreate}>
|
|
||||||
<PlusIcon className="size-4" />
|
|
||||||
创建Workspace
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{workspaceStore.loading ? (
|
{workspaceStore.loading ? (
|
||||||
<div className="text-center py-10 text-muted-foreground">加载中...</div>
|
<div className="text-center py-10 text-muted-foreground">加载中...</div>
|
||||||
) : workspaceStore.list.length === 0 ? (
|
) : workspaceStore.list.length === 0 ? (
|
||||||
<div className="text-center py-10 text-muted-foreground">暂无workspace数据</div>
|
<div className="text-center py-10 text-muted-foreground">暂无workspace数据</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{workspaceStore.list.map((item) => (
|
{workspaceStore.list.map((item) => (
|
||||||
<Card key={item.id}>
|
<Card key={item.id}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{item.title || '未命名'}</CardTitle>
|
<CardTitle>{item.title || '未命名'}</CardTitle>
|
||||||
<CardDescription className="text-xs">ID: {item.id}</CardDescription>
|
<CardDescription className="text-xs">ID: {item.id}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
{item.tags && item.tags.length > 0 && (
|
{item.tags && item.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{item.tags.map((tag, index) => (
|
{item.tags.map((tag, index) => (
|
||||||
<Badge key={index} variant="outline">{tag}</Badge>
|
<Badge key={index} variant="outline">{tag}</Badge>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.summary && (
|
||||||
|
<p className="text-sm text-muted-foreground">{item.summary}</p>
|
||||||
|
)}
|
||||||
|
{item.description && (
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-2">{item.description}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handleEdit(item)}>
|
||||||
|
<PencilIcon className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handleDelete(item.id)}>
|
||||||
|
<TrashIcon className="size-3.5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CardContent>
|
||||||
{item.summary && (
|
</Card>
|
||||||
<p className="text-sm text-muted-foreground">{item.summary}</p>
|
))}
|
||||||
)}
|
</div>
|
||||||
{item.description && (
|
)}
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">{item.description}</p>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2 pt-2">
|
|
||||||
<Button variant="outline" size="sm" onClick={() => handleEdit(item)}>
|
|
||||||
<PencilIcon className="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => handleDelete(item.id)}>
|
|
||||||
<TrashIcon className="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CreateDialog
|
<CreateDialog
|
||||||
open={workspaceStore.showCreateDialog}
|
open={workspaceStore.showCreateDialog}
|
||||||
onClose={() => workspaceStore.setShowCreateDialog(false)}
|
onClose={() => workspaceStore.setShowCreateDialog(false)}
|
||||||
onSubmit={workspaceStore.createItem}
|
onSubmit={workspaceStore.createItem}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditDialog
|
<EditDialog
|
||||||
open={workspaceStore.showEditDialog}
|
open={workspaceStore.showEditDialog}
|
||||||
item={workspaceStore.editingItem}
|
item={workspaceStore.editingItem}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
workspaceStore.setShowEditDialog(false);
|
workspaceStore.setShowEditDialog(false);
|
||||||
workspaceStore.setEditingItem(null);
|
workspaceStore.setEditingItem(null);
|
||||||
}}
|
}}
|
||||||
onSubmit={workspaceStore.updateItem}
|
onSubmit={workspaceStore.updateItem}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</SidebarLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user