generated from kevisual/vite-react-template
Compare commits
9 Commits
bc9ce9e5df
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
518a3f2864 | ||
|
|
477826dcce | ||
|
|
ef08303182 | ||
|
|
389f7a7ad2 | ||
|
|
dd6eff9269 | ||
|
|
9a06364880 | ||
|
|
3f0733a540 | ||
|
|
330accb822 | ||
|
|
469d23b0b9 |
30
README.md
30
README.md
@@ -1,26 +1,12 @@
|
||||
# cnb center
|
||||
|
||||
> cnb 仓库界面很不好用,所以自己写了一个纯调api的界面,方便管理仓库和查看同步状态
|
||||
一个应用工作台
|
||||
|
||||
## 主要功能
|
||||
## 功能
|
||||
|
||||
### git仓库管理功能
|
||||
|
||||
- 创建仓库
|
||||
- 删除仓库(使用cookie)
|
||||
- 同步仓库
|
||||
|
||||
仓库列出用户所有仓库,主体显示仓库名,描述。每一个仓库具备更快捷的功能模块。
|
||||
|
||||
启动是启动云开发环境。
|
||||
|
||||

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

|
||||
|
||||
示例cookie配置
|
||||
```sh
|
||||
CNBSESSION=1770622649.1935321989751226368.0bc7fc786f7052cb2b077c00ded651a5945d46d1e466f4fafa14ede554da14a0;csrfkey=158893308
|
||||
```
|
||||
1. 对话管理
|
||||
2. 仓库管理
|
||||
3. 云开发(cloud-dev)
|
||||
4. 应用管理
|
||||
5. Agent管理
|
||||
6. 配置
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { QueryRouterServer } from '@kevisual/router/browser'
|
||||
import { useContextKey } from '@kevisual/context'
|
||||
import { useGiteaConfigStore } from '@/pages/config/gitea/store'
|
||||
import { Gitea } from '@kevisual/gitea';
|
||||
export const app = useContextKey('app', new QueryRouterServer())
|
||||
|
||||
// import '@kevisual/cnb-ai'
|
||||
@@ -9,16 +7,4 @@ export const app = useContextKey('app', new QueryRouterServer())
|
||||
const url = 'https://kevisual.cn/root/cnb-ai/dist/app.js'
|
||||
setTimeout(() => {
|
||||
import(/* @vite-ignore */url)
|
||||
}, 2000)
|
||||
|
||||
export const gitea = useContextKey('gitea', () => {
|
||||
const state = useGiteaConfigStore.getState()
|
||||
const config = state.config || {}
|
||||
return new Gitea({
|
||||
token: config.GITEA_TOKEN,
|
||||
baseURL: config.GITEA_URL,
|
||||
cors: {
|
||||
baseUrl: 'https://cors.kevisual.cn'
|
||||
}
|
||||
})
|
||||
})
|
||||
}, 2000)
|
||||
281
src/components/a/Sidebar.tsx
Normal file
281
src/components/a/Sidebar.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
'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[]
|
||||
external?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
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) => {
|
||||
// 优先执行 onClick 回调
|
||||
if (item.onClick) {
|
||||
item.onClick()
|
||||
return
|
||||
}
|
||||
|
||||
if (item.isDeveloping) {
|
||||
setDevelopingDialog({ open: true, title: item.title })
|
||||
} else if (item.external && item.path.startsWith('http')) {
|
||||
window.open(item.path, '_blank')
|
||||
} else if (item.path.startsWith('/')) {
|
||||
navigate({ to: item.path })
|
||||
} else {
|
||||
navigate({ href: 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
13
src/components/ui/skeleton.tsx
Normal file
13
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
378
src/modules/mark-api.ts
Normal file
378
src/modules/mark-api.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { createQueryApi } from '@kevisual/query/api';
|
||||
const api = {
|
||||
"mark": {
|
||||
/**
|
||||
* 获取mark列表
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.page - {number} 页码
|
||||
* @param data.pageSize - {number} 每页数量
|
||||
* @param data.search - {string} 搜索关键词
|
||||
* @param data.markType - {string} mark类型,simple,wallnote,md,draw等
|
||||
* @param data.sort - {"DESC" | "ASC"} 排序字段
|
||||
*/
|
||||
"list": {
|
||||
"path": "mark",
|
||||
"key": "list",
|
||||
"description": "获取mark列表",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"page": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "页码",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"pageSize": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "每页数量",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"search": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "搜索关键词",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"markType": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "mark类型,simple,wallnote,md,draw等",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"sort": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "DESC",
|
||||
"description": "排序字段",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DESC",
|
||||
"ASC"
|
||||
],
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取mark版本信息
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
*/
|
||||
"getVersion": {
|
||||
"path": "mark",
|
||||
"key": "getVersion",
|
||||
"description": "获取mark版本信息",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取mark详情
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
*/
|
||||
"get": {
|
||||
"path": "mark",
|
||||
"key": "get",
|
||||
"description": "获取mark详情",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 更新mark内容
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
* @param data.data - {object}
|
||||
*/
|
||||
"update": {
|
||||
"path": "mark",
|
||||
"key": "update",
|
||||
"description": "更新mark内容",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
},
|
||||
"data": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"default": []
|
||||
},
|
||||
"link": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"summary": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"tags",
|
||||
"link",
|
||||
"summary",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 更新mark节点,支持更新和删除操作
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
* @param data.operate - {"update" | "delete"} 节点操作类型,update或delete
|
||||
* @param data.data - {object} 要更新的节点数据
|
||||
*/
|
||||
"updateNode": {
|
||||
"path": "mark",
|
||||
"key": "updateNode",
|
||||
"description": "更新mark节点,支持更新和删除操作",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
},
|
||||
"operate": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "update",
|
||||
"description": "节点操作类型,update或delete",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"update",
|
||||
"delete"
|
||||
],
|
||||
"optional": true
|
||||
},
|
||||
"data": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "节点id"
|
||||
},
|
||||
"node": {
|
||||
"description": "要更新的节点数据"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"node"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"description": "要更新的节点数据"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 批量更新mark节点,支持更新和删除操作
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
* @param data.nodeOperateList - {array} 要更新的节点列表
|
||||
*/
|
||||
"updateNodes": {
|
||||
"path": "mark",
|
||||
"key": "updateNodes",
|
||||
"description": "批量更新mark节点,支持更新和删除操作",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
},
|
||||
"nodeOperateList": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operate": {
|
||||
"default": "update",
|
||||
"description": "节点操作类型,update或delete",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"update",
|
||||
"delete"
|
||||
]
|
||||
},
|
||||
"node": {
|
||||
"description": "要更新的节点数据"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"operate",
|
||||
"node"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"description": "要更新的节点列表"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
*/
|
||||
"delete": {
|
||||
"path": "mark",
|
||||
"key": "delete",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 创建一个新的mark.
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.title - {string} 标题
|
||||
* @param data.tags - {unknown} 标签
|
||||
* @param data.link - {string} 链接
|
||||
* @param data.summary - {string} 摘要
|
||||
* @param data.description - {string} 描述
|
||||
* @param data.markType - {string} mark类型
|
||||
* @param data.config - {unknown} 配置
|
||||
* @param data.data - {unknown} 数据
|
||||
*/
|
||||
"create": {
|
||||
"path": "mark",
|
||||
"key": "create",
|
||||
"description": "创建一个新的mark.",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"title": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "",
|
||||
"description": "标题",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"tags": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"default": [],
|
||||
"description": "标签",
|
||||
"optional": true
|
||||
},
|
||||
"link": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "",
|
||||
"description": "链接",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"summary": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "",
|
||||
"description": "摘要",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"description": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "",
|
||||
"description": "描述",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"markType": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "md",
|
||||
"description": "mark类型",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"config": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": {},
|
||||
"description": "配置",
|
||||
"optional": true
|
||||
},
|
||||
"data": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": {},
|
||||
"description": "数据",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取mark菜单
|
||||
*/
|
||||
"getMenu": {
|
||||
"path": "mark",
|
||||
"key": "getMenu",
|
||||
"description": "获取mark菜单",
|
||||
"metadata": {
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
const queryApi = createQueryApi({ api });
|
||||
|
||||
export { queryApi };
|
||||
239
src/pages/cloud-env/page.tsx
Normal file
239
src/pages/cloud-env/page.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCloudEnvStore } from './store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { SidebarLayout } from '../sidebar/components'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import {
|
||||
Code2,
|
||||
Terminal,
|
||||
MousePointer2,
|
||||
Lock,
|
||||
Radio,
|
||||
Zap,
|
||||
Square,
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Check,
|
||||
Wind,
|
||||
Plane,
|
||||
Rocket,
|
||||
ExternalLink
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { WorkspaceInfo } from '@kevisual/cnb'
|
||||
import clsx from 'clsx'
|
||||
|
||||
type WorkspaceOpen = {
|
||||
url?: string
|
||||
webide?: string
|
||||
jumpUrl?: string
|
||||
remoteSsh?: string
|
||||
jetbrains?: Record<string, string>
|
||||
codebuddy?: string
|
||||
codebuddycn?: string
|
||||
vscode?: string
|
||||
cursor?: string
|
||||
'vscode-insiders'?: string
|
||||
trae?: string
|
||||
'trae-cn'?: string
|
||||
windsurf?: string
|
||||
'windsurf-next'?: string
|
||||
antigravity?: string
|
||||
ssh?: string
|
||||
}
|
||||
|
||||
interface LinkItem {
|
||||
key: string
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
getUrl: (data: WorkspaceOpen) => string | undefined
|
||||
}
|
||||
|
||||
const linkItems: LinkItem[] = [
|
||||
{ key: 'jumpUrl', label: 'Jump', icon: <ExternalLink className="w-5 h-5" />, getUrl: (d) => d.jumpUrl },
|
||||
{ key: 'webide', label: 'Web IDE', icon: <Code2 className="w-5 h-5" />, getUrl: (d) => d.webide },
|
||||
{ key: 'vscode', label: 'VS Code', icon: <Code2 className="w-5 h-5" />, getUrl: (d) => d.vscode },
|
||||
{ key: 'cursor', label: 'Cursor', icon: <MousePointer2 className="w-5 h-5" />, getUrl: (d) => d.cursor },
|
||||
{ key: 'trae-cn', label: 'Trae', icon: <Rocket className="w-5 h-5" />, getUrl: (d) => d['trae-cn'] },
|
||||
{ key: 'windsurf', label: 'Windsurf', icon: <Wind className="w-5 h-5" />, getUrl: (d) => d.windsurf },
|
||||
{ key: 'antigravity', label: 'Antigravity', icon: <Plane className="w-5 h-5" />, getUrl: (d) => d.antigravity },
|
||||
{ key: 'ssh', label: 'SSH', icon: <Lock className="w-5 h-5" />, getUrl: (d) => d.ssh },
|
||||
{ key: 'remoteSsh', label: 'Remote SSH', icon: <Radio className="w-5 h-5" />, getUrl: (d) => d.remoteSsh },
|
||||
{ key: 'codebuddycn', label: 'CodeBuddy', icon: <Zap className="w-5 h-5" />, getUrl: (d) => d.codebuddycn },
|
||||
]
|
||||
|
||||
function LinkCard({ item, workspaceData }: { item: LinkItem; workspaceData: WorkspaceOpen }) {
|
||||
const url = item.getUrl(workspaceData)
|
||||
const [copied, setCopied] = useState(false)
|
||||
if (!url) return null
|
||||
|
||||
const handleClick = () => {
|
||||
if (url.startsWith('ssh') || url.startsWith('cnb')) {
|
||||
return
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setCopied(true)
|
||||
toast.success('已复制')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={clsx(
|
||||
'flex items-center gap-2 p-2.5 rounded-lg border border-neutral-200 transition-all cursor-pointer',
|
||||
'hover:border-neutral-900 hover:bg-neutral-50'
|
||||
)}
|
||||
>
|
||||
<div className="text-neutral-700 shrink-0">{item.icon}</div>
|
||||
<span className="text-sm font-medium text-neutral-900 truncate flex-1">{item.label}</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="p-1 rounded hover:bg-neutral-100 shrink-0"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-green-600" /> : <Copy className="w-4 h-4 text-neutral-400" />}
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-xs break-all">{url}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkspaceCard({ workspace, onStop }: { workspace: WorkspaceInfo; onStop: (ws: WorkspaceInfo) => void }) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [workspaceData, setWorkspaceData] = useState<WorkspaceOpen | null>(null)
|
||||
const getWorkspaceDetail = useCloudEnvStore((state) => state.getWorkspaceDetail)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDetail = async () => {
|
||||
setLoading(true)
|
||||
const data = await getWorkspaceDetail(workspace)
|
||||
setWorkspaceData(data)
|
||||
setLoading(false)
|
||||
}
|
||||
fetchDetail()
|
||||
}, [workspace, getWorkspaceDetail])
|
||||
|
||||
return (
|
||||
<Card className="p-4 space-y-4 border border-neutral-200 bg-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500"></span>
|
||||
</span>
|
||||
<span className="font-medium text-neutral-900">{workspace.slug}</span>
|
||||
</div>
|
||||
{workspace.branch && (
|
||||
<Badge variant="outline" className="text-xs">{workspace.branch}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onStop(workspace)}
|
||||
className="text-red-600 border-red-200 hover:bg-red-600 hover:text-white"
|
||||
>
|
||||
<Square className="w-4 h-4 mr-1" />
|
||||
停止
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="h-12 bg-neutral-100 rounded-lg animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : workspaceData ? (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{linkItems.map((item) => (
|
||||
<LinkCard key={item.key} item={item} workspaceData={workspaceData} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-neutral-400 py-4">暂无链接信息</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CloudEnvPage() {
|
||||
const { workspaceList, loading, getWorkspaceList, stopWorkspace } = useCloudEnvStore()
|
||||
|
||||
useEffect(() => {
|
||||
getWorkspaceList()
|
||||
}, [getWorkspaceList])
|
||||
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900">云端开发环境</h1>
|
||||
<p className="text-neutral-500 mt-1">当前运行中的云端开发环境</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => getWorkspaceList()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={clsx('w-4 h-4 mr-2', loading && 'animate-spin')} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading && workspaceList.length === 0 ? (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-40" />
|
||||
))}
|
||||
</div>
|
||||
) : workspaceList.length === 0 ? (
|
||||
<Card className="p-12 text-center border border-neutral-200">
|
||||
<div className="text-neutral-400 mb-4">
|
||||
<Terminal className="w-12 h-12 mx-auto mb-4" />
|
||||
<p className="text-lg font-medium">暂无运行中的工作区</p>
|
||||
<p className="text-sm mt-1">在仓库管理页面启动工作区即可在此查看</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{workspaceList.map((workspace) => (
|
||||
<WorkspaceCard
|
||||
key={workspace.sn}
|
||||
workspace={workspace}
|
||||
onStop={stopWorkspace}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
)
|
||||
}
|
||||
91
src/pages/cloud-env/store/index.ts
Normal file
91
src/pages/cloud-env/store/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { create } from 'zustand'
|
||||
import { toast } from 'sonner'
|
||||
import { queryApi as cnbApi } from '@/modules/cnb-api'
|
||||
import { WorkspaceInfo } from '@kevisual/cnb'
|
||||
|
||||
type WorkspaceOpen = {
|
||||
url?: string
|
||||
webide?: string
|
||||
jumpUrl?: string
|
||||
remoteSsh?: string
|
||||
jetbrains?: Record<string, string>
|
||||
codebuddy?: string
|
||||
codebuddycn?: string
|
||||
vscode?: string
|
||||
cursor?: string
|
||||
'vscode-insiders'?: string
|
||||
trae?: string
|
||||
'trae-cn'?: string
|
||||
windsurf?: string
|
||||
'windsurf-next'?: string
|
||||
antigravity?: string
|
||||
ssh?: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
workspaceList: WorkspaceInfo[]
|
||||
loading: boolean
|
||||
getWorkspaceList: () => Promise<void>
|
||||
getWorkspaceDetail: (data: WorkspaceInfo) => Promise<WorkspaceOpen | null>
|
||||
stopWorkspace: (workspace: WorkspaceInfo) => Promise<void>
|
||||
}
|
||||
|
||||
export const useCloudEnvStore = create<State>((set, get) => ({
|
||||
workspaceList: [],
|
||||
loading: false,
|
||||
getWorkspaceList: async () => {
|
||||
set({ loading: true })
|
||||
try {
|
||||
const res = await cnbApi.cnb['list-workspace']({
|
||||
status: 'running',
|
||||
pageSize: 100
|
||||
})
|
||||
if (res.code === 200) {
|
||||
const list: WorkspaceInfo[] = res.data?.list || []
|
||||
set({ workspaceList: list })
|
||||
} else {
|
||||
toast.error(res.message || '请求失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取工作区列表失败:', error)
|
||||
toast.error('获取工作区列表失败')
|
||||
} finally {
|
||||
set({ loading: false })
|
||||
}
|
||||
},
|
||||
getWorkspaceDetail: async (workspaceInfo: WorkspaceInfo): Promise<WorkspaceOpen | null> => {
|
||||
try {
|
||||
const res = await cnbApi.cnb['get-workspace']({
|
||||
repo: workspaceInfo.slug,
|
||||
sn: workspaceInfo.sn
|
||||
}) as any
|
||||
if (res.code === 200) {
|
||||
return res.data
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('获取工作区详情失败:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
stopWorkspace: async (workspace: WorkspaceInfo) => {
|
||||
const sn = workspace.sn
|
||||
if (!sn) {
|
||||
toast.error('工作区 SN 不存在')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await cnbApi.cnb['stop-workspace']({ sn })
|
||||
if (res?.code === 200) {
|
||||
toast.success('工作区已停止')
|
||||
// 刷新列表
|
||||
await get().getWorkspaceList()
|
||||
} else {
|
||||
toast.error(res.message || '停止失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止工作区失败:', error)
|
||||
toast.error('停止失败')
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -36,7 +36,7 @@ const api = {
|
||||
"skill": "cnb-login-verify",
|
||||
"title": "CNB 登录验证信息",
|
||||
"summary": "验证 CNB 登录信息是否有效",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -79,7 +79,7 @@ const api = {
|
||||
"skill": "list-repos",
|
||||
"title": "列出cnb代码仓库",
|
||||
"summary": "列出cnb代码仓库, 可选flags参数,如 KnowledgeBase",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -121,7 +121,7 @@ const api = {
|
||||
"skill": "create-repo",
|
||||
"title": "创建代码仓库",
|
||||
"summary": "创建一个新的代码仓库",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -168,7 +168,7 @@ const api = {
|
||||
"skill": "create-repo-file",
|
||||
"title": "在代码仓库中创建文件",
|
||||
"summary": "在代码仓库中创建文件, encoding 可选,默认 raw",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -196,7 +196,7 @@ const api = {
|
||||
"skill": "delete-repo",
|
||||
"title": "删除代码仓库",
|
||||
"summary": "删除一个代码仓库",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -215,7 +215,7 @@ const api = {
|
||||
"skill": "clean-closed-workspace",
|
||||
"title": "清理已关闭的cnb工作空间",
|
||||
"summary": "批量删除已停止的cnb工作空间,释放资源",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -244,7 +244,7 @@ const api = {
|
||||
"description": "流水线ID,例如 cnb-708-1ji9sog7o-001"
|
||||
}
|
||||
},
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -273,7 +273,7 @@ const api = {
|
||||
"description": "流水线ID,例如 cnb-708-1ji9sog7o-001"
|
||||
}
|
||||
},
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -291,7 +291,7 @@ const api = {
|
||||
"skill": "keep-alive-current-workspace",
|
||||
"title": "保持当前工作空间存活",
|
||||
"summary": "保持当前工作空间存活,防止被关闭或释放资源",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -333,7 +333,7 @@ const api = {
|
||||
"skill": "start-workspace",
|
||||
"title": "启动cnb工作空间",
|
||||
"summary": "启动cnb工作空间",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -390,7 +390,7 @@ const api = {
|
||||
"skill": "list-workspace",
|
||||
"title": "列出cnb工作空间",
|
||||
"summary": "列出cnb工作空间列表,支持按状态过滤, status 可选值 running 或 closed",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -424,7 +424,7 @@ const api = {
|
||||
"skill": "get-workspace",
|
||||
"title": "获取工作空间详情",
|
||||
"summary": "获取工作空间详细信息",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -470,7 +470,7 @@ const api = {
|
||||
"skill": "delete-workspace",
|
||||
"title": "删除工作空间",
|
||||
"summary": "删除工作空间,pipelineId 和 sn 二选一",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -506,7 +506,7 @@ const api = {
|
||||
"skill": "stop-workspace",
|
||||
"title": "停止工作空间",
|
||||
"summary": "停止运行中的工作空间",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -535,7 +535,7 @@ const api = {
|
||||
"skill": "get-cnb-port-uri",
|
||||
"title": "获取当前cnb工作空间的port代理uri",
|
||||
"summary": "获取当前cnb工作空间的port代理uri,用于端口转发",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -592,7 +592,7 @@ const api = {
|
||||
"skill": "get-cnb-vscode-uri",
|
||||
"title": "获取当前cnb工作空间的编辑器访问地址",
|
||||
"summary": "获取当前cnb工作空间的vscode代理uri,用于在浏览器中访问vscode,包含多种访问方式,如web、vscode、codebuddy、cursor、ssh",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -620,7 +620,7 @@ const api = {
|
||||
"skill": "set-cnb-cookie",
|
||||
"title": "设置当前cnb工作空间的cookie环境变量",
|
||||
"summary": "设置当前cnb工作空间的cookie环境变量,用于界面操作定制模块功能,例子:CNBSESSION=xxxx;csrfkey=2222xxxx;",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -639,7 +639,7 @@ const api = {
|
||||
"skill": "get-cnb-cookie",
|
||||
"title": "获取当前cnb工作空间的cookie环境变量",
|
||||
"summary": "获取当前cnb工作空间的cookie环境变量,用于界面操作定制模块功能",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -674,7 +674,7 @@ const api = {
|
||||
"skill": "cnb-ai-chat",
|
||||
"title": "调用cnb的知识库ai对话功能进行聊天",
|
||||
"summary": "调用cnb的知识库ai对话功能进行聊天,基于cnb提供的ai能力",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -709,7 +709,7 @@ const api = {
|
||||
"skill": "cnb-rag-query",
|
||||
"title": "调用cnb的知识库RAG查询功能进行问答",
|
||||
"summary": "调用cnb的知识库RAG查询功能进行问答,基于cnb提供的知识库能力",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -779,7 +779,7 @@ const api = {
|
||||
"skill": "list-issues",
|
||||
"title": "查询 Issue 列表",
|
||||
"summary": "查询 Issue 列表",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -847,7 +847,7 @@ const api = {
|
||||
"skill": "create-issue",
|
||||
"title": "创建 Issue",
|
||||
"summary": "创建一个新的 Issue",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
@@ -895,7 +895,7 @@ const api = {
|
||||
"skill": "complete-issue",
|
||||
"title": "完成 CNB的任务Issue",
|
||||
"summary": "完成一个 Issue(将 state 改为 closed)",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<TooltipProvider>
|
||||
<SidebarLayout>
|
||||
<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="p-4 md:p-6 border-b">
|
||||
<h1 className="text-xl md:text-2xl font-bold">CNB 配置</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
配置您的 CNB API 设置。这些设置会保存在浏览器的本地存储中。
|
||||
配置您的 CNB API 设置。
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 md:p-6">
|
||||
@@ -68,7 +68,7 @@ export const ConfigPage = () => {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
用于身份验证的 Cookie 信息。
|
||||
用于身份验证的 Cookie 信息,有效期7天。
|
||||
<a
|
||||
href="https://cnb.cool/kevisual/cnb-live-extension"
|
||||
target="_blank"
|
||||
@@ -102,7 +102,7 @@ export const ConfigPage = () => {
|
||||
</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)
|
||||
}, [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>
|
||||
|
||||
@@ -49,7 +49,7 @@ export function CreateRepoDialog({ open, onOpenChange }: CreateRepoDialogProps)
|
||||
path: '',
|
||||
license: '',
|
||||
description: '',
|
||||
visibility: 'Public'
|
||||
visibility: 'public'
|
||||
})
|
||||
}
|
||||
}, [open, reset])
|
||||
@@ -58,7 +58,10 @@ export function CreateRepoDialog({ open, onOpenChange }: CreateRepoDialogProps)
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const submitData = {
|
||||
...data,
|
||||
path: data.path.trim(),
|
||||
license: data.license.trim(),
|
||||
description: data.description.trim(),
|
||||
visibility: data.visibility,
|
||||
}
|
||||
|
||||
await createRepo(submitData)
|
||||
@@ -104,16 +107,16 @@ export function CreateRepoDialog({ open, onOpenChange }: CreateRepoDialogProps)
|
||||
<Controller
|
||||
name="visibility"
|
||||
control={control}
|
||||
defaultValue="Public"
|
||||
defaultValue="public"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select onValueChange={onChange} value={value}>
|
||||
<SelectTrigger id="visibility">
|
||||
<SelectValue placeholder="选择可见性" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Public">公开 (public)</SelectItem>
|
||||
<SelectItem value="Private">私有 (private)</SelectItem>
|
||||
<SelectItem value="Protected">保护 (protected)</SelectItem>
|
||||
<SelectItem value="public">公开 (public)</SelectItem>
|
||||
<SelectItem value="private">私有 (private)</SelectItem>
|
||||
<SelectItem value="protected">保护 (protected)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useRepoStore } from '../store'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useShallow } from 'zustand/shallow'
|
||||
import { get, set } from 'idb-keyval'
|
||||
import { gitea } from '@/agents/app';
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const SYNC_REPO_STORAGE_KEY = 'sync-repo-mapping'
|
||||
|
||||
export function SyncRepoDialog() {
|
||||
const { syncDialogOpen, setSyncDialogOpen, selectedSyncRepo, buildSync } = useRepoStore(useShallow((state) => ({
|
||||
syncDialogOpen: state.syncDialogOpen,
|
||||
setSyncDialogOpen: state.setSyncDialogOpen,
|
||||
selectedSyncRepo: state.selectedSyncRepo,
|
||||
buildSync: state.buildSync,
|
||||
})))
|
||||
const [toRepo, setToRepo] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const loadSavedMapping = async () => {
|
||||
if (syncDialogOpen && selectedSyncRepo) {
|
||||
const currentPath = selectedSyncRepo.path || ''
|
||||
// 从 idb-keyval 获取存储的映射
|
||||
const mapping = await get<Record<string, string>>(SYNC_REPO_STORAGE_KEY)
|
||||
// 如果有存储的值,使用存储的值,否则使用当前仓库路径
|
||||
setToRepo(mapping?.[currentPath] || currentPath)
|
||||
}
|
||||
}
|
||||
loadSavedMapping()
|
||||
}, [syncDialogOpen, selectedSyncRepo])
|
||||
|
||||
const handleSync = async () => {
|
||||
if (!selectedSyncRepo || !toRepo.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存映射到 idb-keyval
|
||||
const currentPath = selectedSyncRepo.path || ''
|
||||
const mapping = await get<Record<string, string>>(SYNC_REPO_STORAGE_KEY) || {}
|
||||
mapping[currentPath] = toRepo
|
||||
await set(SYNC_REPO_STORAGE_KEY, mapping)
|
||||
|
||||
await buildSync(selectedSyncRepo, { toRepo })
|
||||
setSyncDialogOpen(false)
|
||||
}
|
||||
const onCreateRepo = async () => {
|
||||
if (!toRepo.trim()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await gitea.repo.createRepo({ name: toRepo })
|
||||
if (res.code !== 200 && res.code !== 409) {
|
||||
// 409 表示仓库已存在,可以继续同步
|
||||
throw new Error(`${res.message}`)
|
||||
}
|
||||
if (res.code === 200) {
|
||||
toast.success('仓库创建成功,正在同步...')
|
||||
} else {
|
||||
toast.warning('仓库已存在,正在同步...')
|
||||
}
|
||||
handleSync()
|
||||
} catch (error) {
|
||||
console.error('创建仓库失败:', error)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Dialog open={syncDialogOpen} onOpenChange={setSyncDialogOpen}>
|
||||
<DialogContent className="sm:max-w-125">
|
||||
<DialogHeader>
|
||||
<DialogTitle>同步仓库到 Gitea</DialogTitle>
|
||||
<DialogDescription>
|
||||
将仓库 <span className="font-semibold text-neutral-900">{selectedSyncRepo?.path}</span> 同步到目标仓库
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="toRepo">目标仓库路径</Label>
|
||||
<Input
|
||||
id="toRepo"
|
||||
placeholder="例如: kevisual/my-repo"
|
||||
value={toRepo}
|
||||
onChange={(e) => setToRepo(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-neutral-500">
|
||||
格式: owner/repo-name
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSyncDialogOpen(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onCreateRepo} disabled={!toRepo.trim()}>
|
||||
先创建仓库再同步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSync}
|
||||
disabled={!toRepo.trim()}
|
||||
>
|
||||
开始同步
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -15,17 +15,18 @@ import type { WorkspaceOpen } from '../store'
|
||||
import {
|
||||
Code2,
|
||||
Terminal,
|
||||
Sparkles,
|
||||
MousePointer2,
|
||||
Box,
|
||||
Lock,
|
||||
Radio,
|
||||
Bot,
|
||||
Zap,
|
||||
Copy,
|
||||
Check,
|
||||
Square,
|
||||
Link
|
||||
Link,
|
||||
ExternalLink,
|
||||
Wind,
|
||||
Plane,
|
||||
Rocket
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
@@ -221,67 +222,74 @@ export function WorkspaceDetailDialog() {
|
||||
workspaceSecretLink: state.workspaceSecretLink
|
||||
})))
|
||||
const linkItems: LinkItem[] = [
|
||||
{
|
||||
key: 'jumpUrl' as LinkItemKey,
|
||||
label: 'Jump',
|
||||
icon: <ExternalLink className="w-5 h-5" />,
|
||||
order: 1,
|
||||
getUrl: (data) => data.jumpUrl
|
||||
},
|
||||
{
|
||||
key: 'webide' as LinkItemKey,
|
||||
label: 'Web IDE',
|
||||
icon: <Code2 className="w-5 h-5" />,
|
||||
order: 1,
|
||||
order: 2,
|
||||
getUrl: (data) => data.webide
|
||||
},
|
||||
{
|
||||
key: 'vscode' as LinkItemKey,
|
||||
label: 'VS Code',
|
||||
icon: <Code2 className="w-5 h-5" />,
|
||||
order: 2,
|
||||
order: 3,
|
||||
getUrl: (data) => data.vscode
|
||||
},
|
||||
{
|
||||
key: 'vscode-insiders' as LinkItemKey,
|
||||
label: 'VS Code Insiders',
|
||||
icon: <Sparkles className="w-5 h-5" />,
|
||||
order: 5,
|
||||
getUrl: (data) => data['vscode-insiders']
|
||||
},
|
||||
{
|
||||
key: 'cursor' as LinkItemKey,
|
||||
label: 'Cursor',
|
||||
icon: <MousePointer2 className="w-5 h-5" />,
|
||||
order: 6,
|
||||
order: 4,
|
||||
getUrl: (data) => data.cursor
|
||||
},
|
||||
{
|
||||
key: 'jetbrains' as LinkItemKey,
|
||||
label: 'JetBrains IDEs',
|
||||
icon: <Box className="w-5 h-5" />,
|
||||
key: 'trae-cn' as LinkItemKey,
|
||||
label: 'Trae',
|
||||
icon: <Rocket className="w-5 h-5" />,
|
||||
order: 5,
|
||||
getUrl: (data) => data['trae-cn']
|
||||
},
|
||||
{
|
||||
key: 'windsurf' as LinkItemKey,
|
||||
label: 'Windsurf',
|
||||
icon: <Wind className="w-5 h-5" />,
|
||||
order: 6,
|
||||
getUrl: (data) => data.windsurf
|
||||
},
|
||||
{
|
||||
key: 'antigravity' as LinkItemKey,
|
||||
label: 'Antigravity',
|
||||
icon: <Plane className="w-5 h-5" />,
|
||||
order: 7,
|
||||
getUrl: (data) => Object.values(data.jetbrains || {}).find(Boolean)
|
||||
getUrl: (data) => data.antigravity
|
||||
},
|
||||
{
|
||||
key: 'ssh' as LinkItemKey,
|
||||
label: 'SSH',
|
||||
icon: <Lock className="w-5 h-5" />,
|
||||
order: 4,
|
||||
order: 9,
|
||||
getUrl: (data) => data.ssh
|
||||
},
|
||||
{
|
||||
key: 'remoteSsh' as LinkItemKey,
|
||||
label: 'Remote SSH',
|
||||
icon: <Radio className="w-5 h-5" />,
|
||||
order: 8,
|
||||
order: 10,
|
||||
getUrl: (data) => data.remoteSsh
|
||||
},
|
||||
{
|
||||
key: 'codebuddy' as LinkItemKey,
|
||||
label: 'CodeBuddy',
|
||||
icon: <Bot className="w-5 h-5" />,
|
||||
order: 9,
|
||||
getUrl: (data) => data.codebuddy
|
||||
},
|
||||
{
|
||||
key: 'codebuddycn' as LinkItemKey,
|
||||
label: 'CodeBuddy CN',
|
||||
label: 'CodeBuddy',
|
||||
icon: <Zap className="w-5 h-5" />,
|
||||
order: 3,
|
||||
order: 11,
|
||||
getUrl: (data) => data.codebuddycn
|
||||
},
|
||||
].sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
||||
@@ -5,7 +5,6 @@ import { RepoCard } from './components/RepoCard'
|
||||
import { EditRepoDialog } from './modules/EditRepoDialog'
|
||||
import { CreateRepoDialog } from './modules/CreateRepoDialog'
|
||||
import { WorkspaceDetailDialog } from './modules/WorkspaceDetailDialog'
|
||||
import { SyncRepoDialog } from './modules/SyncRepoDialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ExternalLinkIcon, Plus, RefreshCw, Search, Settings } from 'lucide-react'
|
||||
@@ -13,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) => ({
|
||||
@@ -23,19 +24,15 @@ export const App = () => {
|
||||
setShowCreateDialog: state.setShowCreateDialog,
|
||||
})))
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [filterDev, setFilterDev] = useState(() => {
|
||||
const saved = localStorage.getItem('repos-filter-dev')
|
||||
return saved === 'true'
|
||||
})
|
||||
const navigate = useNavigate();
|
||||
const me = useLayoutStore(state => state.me)
|
||||
const configStore = useConfigStore(useShallow(state => ({ checkConfig: state.checkConfig })))
|
||||
useEffect(() => {
|
||||
refresh({ showTips: false })
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (me && me.id) {
|
||||
configStore.checkConfig({ isUser: true, reload: true })
|
||||
}
|
||||
}, [me])
|
||||
|
||||
|
||||
|
||||
const appList = useMemo(() => {
|
||||
const sortedList = [...list].sort((a, b) => {
|
||||
const aActive = workspaceList.some(ws => ws.slug === a.path)
|
||||
@@ -46,11 +43,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
|
||||
@@ -58,101 +63,124 @@ 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) => {
|
||||
const value = checked === true
|
||||
setFilterDev(value)
|
||||
localStorage.setItem('repos-filter-dev', String(value))
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -176,7 +204,6 @@ export const CommonRepoDialog = () => {
|
||||
onOpenChange={setShowCreateDialog}
|
||||
/>
|
||||
<WorkspaceDetailDialog />
|
||||
<SyncRepoDialog />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,28 +57,11 @@ export const createDevConfig = (params: { repo?: string, event?: string, branch?
|
||||
return `##### 配置开始,保留注释 #####
|
||||
.common_env: &common_env
|
||||
env:
|
||||
# 使用环境变量管理密钥,推荐使用密钥仓库管理密钥, 详情见 readme.md
|
||||
# 使用仓库密钥时,注释
|
||||
## 可选 API-Key 配置(按需取消注释)
|
||||
# MINIMAX_API_KEY: '' # Minimax 模型
|
||||
# ZHIPU_API_KEY: '' # 智谱 AI
|
||||
# BAILIAN_CODE_API_KEY: '' # 阿里云百炼
|
||||
# VOLCENGINE_API_KEY: '' # 火山引擎
|
||||
# CNB_API_KEY: '' # CNB API
|
||||
# CNB_COOKIE: '' # 可选配置,用cnb.cool的cookie
|
||||
|
||||
# 可选应用配置
|
||||
# FEISHU_APP_ID: '' # 飞书应用 ID
|
||||
# FEISHU_APP_SECRET: '' # 飞书应用密钥
|
||||
|
||||
USERNAME: root
|
||||
ASSISTANT_CONFIG_DIR: /workspace/kevisual # ASSISTANT_CONFIG_DIR 环境变量指定了配置文件所在的目录
|
||||
# CNB_KEVISUAL_ORG: kevisual # 私密仓库使用环境配置(默认即可,默认为当前用户组CNB_GROUP_SLUG)
|
||||
# CNB_KEVISUAL_APP: assistant-app # 可选配置(默认即可)
|
||||
# CNB_OPENCLAW: openclaw # 仓库名(默认即可)
|
||||
# CNB_OPENWEBUI: open-webui # 仓库名(默认即可)
|
||||
imports:
|
||||
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
|
||||
- https://cnb.cool/\${CNB_GROUP_SLUG}/env/-/blob/main/.env
|
||||
# - https://cnb.cool/\${CNB_GROUP_SLUG}/env/-/blob/main/ssh.yml
|
||||
# - https://cnb.cool/\${CNB_GROUP_SLUG}/env/-/blob/main/ssh-config.yml
|
||||
|
||||
##### 配置结束 #####
|
||||
|
||||
@@ -89,19 +72,29 @@ ${branch}:
|
||||
services:
|
||||
- vscode
|
||||
- docker
|
||||
runner:
|
||||
cpus: 16
|
||||
#tags: cnb:arch:amd64:gpu
|
||||
imports: !reference [.common_env, imports]
|
||||
env: !reference [.common_env, env]
|
||||
runner:
|
||||
cpus: $RUN_CPU
|
||||
#tags: cnb:arch:amd64:gpu
|
||||
stages:
|
||||
- name: 安装dev-cnb的仓库代码模块
|
||||
script: |
|
||||
cd /workspace && find . -mindepth 1 -delete
|
||||
git init
|
||||
git remote add origin https://cnb.cool/kevisual/dev-cnb
|
||||
git fetch origin main
|
||||
git reset --hard origin/main
|
||||
- name: 启动nginx
|
||||
script: nginx
|
||||
- name: 启动搜索服务
|
||||
script: zsh -i -c 'bun src/cli.ts init start-meilisearch'
|
||||
- name: 初始化开发机
|
||||
script: zsh /workspace/scripts/init.sh
|
||||
script: zsh -i -c 'bun run start'
|
||||
- name: 启动当前工作区
|
||||
script: zsh -i -c 'cloud cnb keep-alive-current-workspace'
|
||||
# endStages:
|
||||
# - name: 结束阶段
|
||||
# script: bun /workspace/scripts/end.ts
|
||||
|
||||
# script: zsh -i -c 'bun run end'
|
||||
`
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { query } from '@/modules/query';
|
||||
import { toast } from 'sonner';
|
||||
import { queryApi as cnbApi } from '@/modules/cnb-api'
|
||||
import { WorkspaceInfo } from '@kevisual/cnb'
|
||||
import { createBuildConfig, createCommitBlankConfig, createDevConfig } from './build';
|
||||
import { createCommitBlankConfig, createDevConfig } from './build';
|
||||
import { useLayoutStore } from '@/pages/auth/store';
|
||||
import { Query } from '@kevisual/query';
|
||||
interface DisplayModule {
|
||||
@@ -94,7 +94,6 @@ type State = {
|
||||
setSyncDialogOpen: (open: boolean) => void;
|
||||
selectedSyncRepo: Data | null;
|
||||
setSelectedSyncRepo: (repo: Data | null) => void;
|
||||
buildSync: (data: Partial<Data>, params: { toRepo?: string, fromRepo?: string }) => Promise<any>;
|
||||
buildUpdate: (data: Partial<Data>, params?: any) => Promise<any>;
|
||||
getItem: (repo: string) => Promise<any>;
|
||||
buildConfig: BuildConfig | null;
|
||||
@@ -525,28 +524,6 @@ export const useRepoStore = create<State>((set, get) => {
|
||||
}
|
||||
},
|
||||
workspaceLink: {},
|
||||
buildSync: async (data, params) => {
|
||||
const repo = data.path!;
|
||||
const toRepo = params.toRepo;
|
||||
const fromRepo = params.fromRepo;
|
||||
if (!toRepo && !fromRepo) {
|
||||
toast.error('请选择同步的目标仓库或来源仓库')
|
||||
return;
|
||||
}
|
||||
let event = toRepo ? 'api_trigger_sync_to_gitea' : 'api_trigger_sync_from_gitea';
|
||||
const res = await cnbApi.cnb['cloud-build']({
|
||||
repo: toRepo! || fromRepo!,
|
||||
branch: 'main',
|
||||
env: {} as any,
|
||||
event: event,
|
||||
config: createBuildConfig({ repo: toRepo! || fromRepo! }),
|
||||
})
|
||||
if (res.code === 200) {
|
||||
toast.success('同步提交成功')
|
||||
} else {
|
||||
toast.error(res.message || '同步提交失败')
|
||||
}
|
||||
},
|
||||
buildUpdate: async (data) => {
|
||||
const res = await cnbApi.cnb['cloud-build']({
|
||||
repo: data.path!,
|
||||
@@ -569,16 +546,22 @@ export const useRepoStore = create<State>((set, get) => {
|
||||
})
|
||||
|
||||
export type WorkspaceOpen = {
|
||||
codebuddy: string;
|
||||
codebuddycn: string;
|
||||
cursor: string;
|
||||
jetbrains: Record<string, string>;
|
||||
jumpUrl: string;
|
||||
remoteSsh: string;
|
||||
ssh: string;
|
||||
vscode: string;
|
||||
'vscode-insiders': string;
|
||||
webide: string;
|
||||
url?: string
|
||||
webide?: string
|
||||
jumpUrl?: string
|
||||
remoteSsh?: string
|
||||
jetbrains?: Record<string, string>
|
||||
codebuddy?: string
|
||||
codebuddycn?: string
|
||||
vscode?: string
|
||||
cursor?: string
|
||||
'vscode-insiders'?: string
|
||||
trae?: string
|
||||
'trae-cn'?: string
|
||||
windsurf?: string
|
||||
'windsurf-next'?: string
|
||||
antigravity?: string
|
||||
ssh?: string
|
||||
}
|
||||
const openWorkspace = (workspace: WorkspaceInfo, params: { vscode?: boolean, ssh?: boolean }) => {
|
||||
const openVsCode = params?.vscode ?? true;
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
40
src/pages/sidebar/components/Sidebar.tsx
Normal file
40
src/pages/sidebar/components/Sidebar.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { FolderKanban, LayoutDashboard, Settings, PlayCircle, Cloud } 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: '/cloud-env',
|
||||
icon: <Cloud 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
105
src/pages/workspaces/components/CreateDialog.tsx
Normal file
105
src/pages/workspaces/components/CreateDialog.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { TagInput } from "./TagInput";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export function CreateDialog({ open, onClose, onSubmit }: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||
}) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [link, setLink] = useState('');
|
||||
const [summary, setSummary] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!title.trim()) {
|
||||
alert('请输入标题');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onSubmit({
|
||||
title: title.trim(),
|
||||
tags,
|
||||
link: link.trim(),
|
||||
summary: summary.trim(),
|
||||
description: description.trim(),
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-lg!">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建Workspace</DialogTitle>
|
||||
<DialogDescription>填写以下信息创建一个新的Workspace</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-title">标题</Label>
|
||||
<Input
|
||||
id="create-title"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>标签</Label>
|
||||
<TagInput value={tags} onChange={setTags} placeholder="输入标签后按回车添加" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-link">链接</Label>
|
||||
<Input
|
||||
id="create-link"
|
||||
value={link}
|
||||
onChange={e => setLink(e.target.value)}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-summary">摘要</Label>
|
||||
<Input
|
||||
id="create-summary"
|
||||
value={summary}
|
||||
onChange={e => setSummary(e.target.value)}
|
||||
placeholder="简要描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-description">描述</Label>
|
||||
<Textarea
|
||||
id="create-description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="详细描述..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={submitting}>取消</Button>
|
||||
<Button variant="outline" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? '创建中...' : '创建'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
117
src/pages/workspaces/components/EditDialog.tsx
Normal file
117
src/pages/workspaces/components/EditDialog.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { TagInput } from "./TagInput";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export function EditDialog({ open, item, onClose, onSubmit }: {
|
||||
open: boolean;
|
||||
item: any;
|
||||
onClose: () => void;
|
||||
onSubmit: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||
}) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [link, setLink] = useState('');
|
||||
const [summary, setSummary] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && item) {
|
||||
setTitle(item.title || '');
|
||||
setTags(item.tags || []);
|
||||
setLink(item.link || '');
|
||||
setSummary(item.summary || '');
|
||||
setDescription(item.description || '');
|
||||
}
|
||||
}, [open, item]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!title.trim()) {
|
||||
alert('请输入标题');
|
||||
return;
|
||||
}
|
||||
if (!item?.id) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onSubmit(item.id, {
|
||||
title: title.trim(),
|
||||
tags,
|
||||
link: link.trim(),
|
||||
summary: summary.trim(),
|
||||
description: description.trim(),
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-lg!">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑Workspace</DialogTitle>
|
||||
<DialogDescription>修改Workspace信息</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-title">标题</Label>
|
||||
<Input
|
||||
id="edit-title"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>标签</Label>
|
||||
<TagInput value={tags} onChange={setTags} placeholder="输入标签后按回车添加" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-link">链接</Label>
|
||||
<Input
|
||||
id="edit-link"
|
||||
value={link}
|
||||
onChange={e => setLink(e.target.value)}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-summary">摘要</Label>
|
||||
<Input
|
||||
id="edit-summary"
|
||||
value={summary}
|
||||
onChange={e => setSummary(e.target.value)}
|
||||
placeholder="简要描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-description">描述</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="详细描述..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={submitting}>取消</Button>
|
||||
<Button variant="outline" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
53
src/pages/workspaces/components/TagInput.tsx
Normal file
53
src/pages/workspaces/components/TagInput.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
export function TagInput({ value, onChange, placeholder }: {
|
||||
value: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && input.trim()) {
|
||||
e.preventDefault();
|
||||
if (!value.includes(input.trim())) {
|
||||
onChange([...value, input.trim()]);
|
||||
}
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (tag: string) => {
|
||||
onChange(value.filter(t => t !== tag));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder || '输入标签后按回车添加'}
|
||||
/>
|
||||
{value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{value.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="gap-1">
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(tag)}
|
||||
className="hover:text-destructive"
|
||||
>
|
||||
<XIcon className="size-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/workspaces/components/index.ts
Normal file
3
src/pages/workspaces/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CreateDialog } from "./CreateDialog";
|
||||
export { EditDialog } from "./EditDialog";
|
||||
export { TagInput } from "./TagInput";
|
||||
@@ -1,29 +1,152 @@
|
||||
import { useWorkspaceStore, type WorkspaceState } from "./store";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import { useMarkStore, useWorkspaceStore } from "./store";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} 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 markStore = useMarkStore(useShallow(state => {
|
||||
const workspaceStore = useWorkspaceStore(useShallow((state: WorkspaceState) => {
|
||||
return {
|
||||
init: state.init,
|
||||
list: state.list,
|
||||
loading: state.loading,
|
||||
getList: state.getList,
|
||||
createItem: state.createItem,
|
||||
updateItem: state.updateItem,
|
||||
deleteItem: state.deleteItem,
|
||||
showCreateDialog: state.showCreateDialog,
|
||||
setShowCreateDialog: state.setShowCreateDialog,
|
||||
showEditDialog: state.showEditDialog,
|
||||
setShowEditDialog: state.setShowEditDialog,
|
||||
editingItem: state.editingItem,
|
||||
setEditingItem: state.setEditingItem,
|
||||
}
|
||||
}));
|
||||
const workspaceStore = useWorkspaceStore(useShallow(state => {
|
||||
return {
|
||||
edit: state.edit,
|
||||
setEdit: state.setEdit,
|
||||
}
|
||||
}));
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
markStore.init('cnb');
|
||||
}, []);
|
||||
console.log('markStore.list', markStore.list);
|
||||
workspaceStore.getList({ search });
|
||||
}, [search]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('确定要删除这个workspace吗?')) {
|
||||
await workspaceStore.deleteItem(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
workspaceStore.getList({ search });
|
||||
};
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
workspaceStore.setEditingItem(item);
|
||||
workspaceStore.setShowEditDialog(true);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
workspaceStore.setShowCreateDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Workspaces</h1>
|
||||
<p>This is the workspaces page.</p>
|
||||
</div>
|
||||
<SidebarLayout>
|
||||
<div className="p-5">
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h1 className="text-2xl font-semibold">Workspaces</h1>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索workspace..."
|
||||
value={search}
|
||||
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>
|
||||
|
||||
{workspaceStore.loading ? (
|
||||
<div className="text-center py-10 text-muted-foreground">加载中...</div>
|
||||
) : workspaceStore.list.length === 0 ? (
|
||||
<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">
|
||||
{workspaceStore.list.map((item) => (
|
||||
<Card key={item.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title || '未命名'}</CardTitle>
|
||||
<CardDescription className="text-xs">ID: {item.id}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{item.tags.map((tag, index) => (
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateDialog
|
||||
open={workspaceStore.showCreateDialog}
|
||||
onClose={() => workspaceStore.setShowCreateDialog(false)}
|
||||
onSubmit={workspaceStore.createItem}
|
||||
/>
|
||||
|
||||
<EditDialog
|
||||
open={workspaceStore.showEditDialog}
|
||||
item={workspaceStore.editingItem}
|
||||
onClose={() => {
|
||||
workspaceStore.setShowEditDialog(false);
|
||||
workspaceStore.setEditingItem(null);
|
||||
}}
|
||||
onSubmit={workspaceStore.updateItem}
|
||||
/>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,13 +2,155 @@ import { useMarkStore } from '@kevisual/api/store-mark';
|
||||
export { useMarkStore }
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { queryApi } from '@/modules/mark-api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type WorkspaceItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
tags?: string[];
|
||||
link?: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
type WorkspaceState = {
|
||||
edit: boolean;
|
||||
setEdit: (edit: boolean) => void;
|
||||
list: WorkspaceItem[];
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
// 弹窗状态
|
||||
showCreateDialog: boolean;
|
||||
setShowCreateDialog: (show: boolean) => void;
|
||||
showEditDialog: boolean;
|
||||
setShowEditDialog: (show: boolean) => void;
|
||||
editingItem: WorkspaceItem | null;
|
||||
setEditingItem: (item: WorkspaceItem | null) => void;
|
||||
// 数据操作
|
||||
getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise<void>;
|
||||
createItem: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||
updateItem: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||
deleteItem: (id: string) => Promise<void>;
|
||||
getItem: (id: string) => Promise<WorkspaceItem | null>;
|
||||
}
|
||||
|
||||
export const useWorkspaceStore = create<WorkspaceState>((set) => ({
|
||||
export type { WorkspaceState, WorkspaceItem };
|
||||
|
||||
export const useWorkspaceStore = create<WorkspaceState>((set, get) => ({
|
||||
edit: false,
|
||||
setEdit: (edit) => set({ edit }),
|
||||
}));
|
||||
list: [],
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
showCreateDialog: false,
|
||||
setShowCreateDialog: (show) => set({ showCreateDialog: show }),
|
||||
showEditDialog: false,
|
||||
setShowEditDialog: (show) => set({ showEditDialog: show }),
|
||||
editingItem: null,
|
||||
setEditingItem: (item) => set({ editingItem: item }),
|
||||
|
||||
getList: async (params = {}) => {
|
||||
const { page = 1, pageSize = 20, search } = params;
|
||||
set({ loading: true });
|
||||
try {
|
||||
const res = await queryApi.mark.list({
|
||||
markType: 'cnb',
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
sort: 'DESC'
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data?.list || [] });
|
||||
} else {
|
||||
toast.error(res.message || '获取列表失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取workspace列表失败', e);
|
||||
toast.error('获取列表失败');
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
createItem: async (data) => {
|
||||
try {
|
||||
const res = await queryApi.mark.create({
|
||||
title: data.title,
|
||||
markType: 'cnb',
|
||||
tags: data.tags || [],
|
||||
link: data.link || '',
|
||||
summary: data.summary || '',
|
||||
description: data.description || ''
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('创建成功');
|
||||
get().getList();
|
||||
set({ showCreateDialog: false });
|
||||
} else {
|
||||
toast.error(res.message || '创建失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('创建失败', e);
|
||||
toast.error('创建失败');
|
||||
}
|
||||
},
|
||||
|
||||
updateItem: async (id, data) => {
|
||||
try {
|
||||
const res = await queryApi.mark.update({
|
||||
id,
|
||||
data: {
|
||||
title: data.title || '',
|
||||
tags: data.tags || [],
|
||||
link: data.link || '',
|
||||
summary: data.summary || '',
|
||||
description: data.description || ''
|
||||
}
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('更新成功');
|
||||
get().getList();
|
||||
set({ showEditDialog: false, editingItem: null });
|
||||
} else {
|
||||
toast.error(res.message || '更新失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('更新失败', e);
|
||||
toast.error('更新失败');
|
||||
}
|
||||
},
|
||||
|
||||
deleteItem: async (id) => {
|
||||
try {
|
||||
const res = await queryApi.mark.delete({ id });
|
||||
if (res.code === 200) {
|
||||
toast.success('删除成功');
|
||||
get().getList();
|
||||
} else {
|
||||
toast.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('删除失败', e);
|
||||
toast.error('删除失败');
|
||||
}
|
||||
},
|
||||
|
||||
getItem: async (id) => {
|
||||
try {
|
||||
const res = await queryApi.mark.get({ id });
|
||||
if (res.code === 200) {
|
||||
return res.data;
|
||||
} else {
|
||||
toast.error(res.message || '获取详情失败');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取详情失败', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as WorkspacesIndexRouteImport } from './routes/workspaces/index'
|
||||
import { Route as RepoIndexRouteImport } from './routes/repo/index'
|
||||
import { Route as ConfigIndexRouteImport } from './routes/config/index'
|
||||
import { Route as CloudEnvIndexRouteImport } from './routes/cloud-env/index'
|
||||
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
@@ -47,6 +48,11 @@ const ConfigIndexRoute = ConfigIndexRouteImport.update({
|
||||
path: '/config/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const CloudEnvIndexRoute = CloudEnvIndexRouteImport.update({
|
||||
id: '/cloud-env/',
|
||||
path: '/cloud-env/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ConfigGiteaRoute = ConfigGiteaRouteImport.update({
|
||||
id: '/config/gitea',
|
||||
path: '/config/gitea',
|
||||
@@ -58,6 +64,7 @@ export interface FileRoutesByFullPath {
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/cloud-env/': typeof CloudEnvIndexRoute
|
||||
'/config/': typeof ConfigIndexRoute
|
||||
'/repo/': typeof RepoIndexRoute
|
||||
'/workspaces/': typeof WorkspacesIndexRoute
|
||||
@@ -67,6 +74,7 @@ export interface FileRoutesByTo {
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/cloud-env': typeof CloudEnvIndexRoute
|
||||
'/config': typeof ConfigIndexRoute
|
||||
'/repo': typeof RepoIndexRoute
|
||||
'/workspaces': typeof WorkspacesIndexRoute
|
||||
@@ -77,6 +85,7 @@ export interface FileRoutesById {
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/cloud-env/': typeof CloudEnvIndexRoute
|
||||
'/config/': typeof ConfigIndexRoute
|
||||
'/repo/': typeof RepoIndexRoute
|
||||
'/workspaces/': typeof WorkspacesIndexRoute
|
||||
@@ -88,6 +97,7 @@ export interface FileRouteTypes {
|
||||
| '/demo'
|
||||
| '/login'
|
||||
| '/config/gitea'
|
||||
| '/cloud-env/'
|
||||
| '/config/'
|
||||
| '/repo/'
|
||||
| '/workspaces/'
|
||||
@@ -97,6 +107,7 @@ export interface FileRouteTypes {
|
||||
| '/demo'
|
||||
| '/login'
|
||||
| '/config/gitea'
|
||||
| '/cloud-env'
|
||||
| '/config'
|
||||
| '/repo'
|
||||
| '/workspaces'
|
||||
@@ -106,6 +117,7 @@ export interface FileRouteTypes {
|
||||
| '/demo'
|
||||
| '/login'
|
||||
| '/config/gitea'
|
||||
| '/cloud-env/'
|
||||
| '/config/'
|
||||
| '/repo/'
|
||||
| '/workspaces/'
|
||||
@@ -116,6 +128,7 @@ export interface RootRouteChildren {
|
||||
DemoRoute: typeof DemoRoute
|
||||
LoginRoute: typeof LoginRoute
|
||||
ConfigGiteaRoute: typeof ConfigGiteaRoute
|
||||
CloudEnvIndexRoute: typeof CloudEnvIndexRoute
|
||||
ConfigIndexRoute: typeof ConfigIndexRoute
|
||||
RepoIndexRoute: typeof RepoIndexRoute
|
||||
WorkspacesIndexRoute: typeof WorkspacesIndexRoute
|
||||
@@ -165,6 +178,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ConfigIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/cloud-env/': {
|
||||
id: '/cloud-env/'
|
||||
path: '/cloud-env'
|
||||
fullPath: '/cloud-env/'
|
||||
preLoaderRoute: typeof CloudEnvIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/config/gitea': {
|
||||
id: '/config/gitea'
|
||||
path: '/config/gitea'
|
||||
@@ -180,6 +200,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
DemoRoute: DemoRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
ConfigGiteaRoute: ConfigGiteaRoute,
|
||||
CloudEnvIndexRoute: CloudEnvIndexRoute,
|
||||
ConfigIndexRoute: ConfigIndexRoute,
|
||||
RepoIndexRoute: RepoIndexRoute,
|
||||
WorkspacesIndexRoute: WorkspacesIndexRoute,
|
||||
|
||||
10
src/routes/cloud-env/index.tsx
Normal file
10
src/routes/cloud-env/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/cloud-env/page'
|
||||
|
||||
export const Route = createFileRoute('/cloud-env/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
Reference in New Issue
Block a user