generated from kevisual/vite-react-template
Compare commits
3 Commits
389f7a7ad2
...
518a3f2864
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
518a3f2864 | ||
|
|
477826dcce | ||
|
|
ef08303182 |
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
1. 对话管理
|
1. 对话管理
|
||||||
2. 仓库管理
|
2. 仓库管理
|
||||||
3. 云开发
|
3. 云开发(cloud-dev)
|
||||||
4. 应用管理
|
4. 应用管理
|
||||||
5. Agent管理
|
5. Agent管理
|
||||||
6. 配置
|
6. 配置
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { QueryRouterServer } from '@kevisual/router/browser'
|
import { QueryRouterServer } from '@kevisual/router/browser'
|
||||||
import { useContextKey } from '@kevisual/context'
|
import { useContextKey } from '@kevisual/context'
|
||||||
import { useGiteaConfigStore } from '@/pages/config/gitea/store'
|
|
||||||
import { Gitea } from '@kevisual/gitea';
|
|
||||||
export const app = useContextKey('app', new QueryRouterServer())
|
export const app = useContextKey('app', new QueryRouterServer())
|
||||||
|
|
||||||
// import '@kevisual/cnb-ai'
|
// 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'
|
const url = 'https://kevisual.cn/root/cnb-ai/dist/app.js'
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
import(/* @vite-ignore */url)
|
import(/* @vite-ignore */url)
|
||||||
}, 2000)
|
}, 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'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useNavigate, useLocation } from '@tanstack/react-router'
|
import { useNavigate, useLocation } from '@tanstack/react-router'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +22,8 @@ export interface NavItem {
|
|||||||
badge?: string
|
badge?: string
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
children?: NavItem[]
|
children?: NavItem[]
|
||||||
|
external?: boolean
|
||||||
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarProps {
|
export interface SidebarProps {
|
||||||
@@ -71,10 +72,20 @@ export function Sidebar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleNavClick = (item: NavItem) => {
|
const handleNavClick = (item: NavItem) => {
|
||||||
|
// 优先执行 onClick 回调
|
||||||
|
if (item.onClick) {
|
||||||
|
item.onClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (item.isDeveloping) {
|
if (item.isDeveloping) {
|
||||||
setDevelopingDialog({ open: true, title: item.title })
|
setDevelopingDialog({ open: true, title: item.title })
|
||||||
} else {
|
} else if (item.external && item.path.startsWith('http')) {
|
||||||
|
window.open(item.path, '_blank')
|
||||||
|
} else if (item.path.startsWith('/')) {
|
||||||
navigate({ to: item.path })
|
navigate({ to: item.path })
|
||||||
|
} else {
|
||||||
|
navigate({ href: item.path })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 }
|
||||||
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('停止失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
@@ -15,17 +15,18 @@ import type { WorkspaceOpen } from '../store'
|
|||||||
import {
|
import {
|
||||||
Code2,
|
Code2,
|
||||||
Terminal,
|
Terminal,
|
||||||
Sparkles,
|
|
||||||
MousePointer2,
|
MousePointer2,
|
||||||
Box,
|
|
||||||
Lock,
|
Lock,
|
||||||
Radio,
|
Radio,
|
||||||
Bot,
|
|
||||||
Zap,
|
Zap,
|
||||||
Copy,
|
Copy,
|
||||||
Check,
|
Check,
|
||||||
Square,
|
Square,
|
||||||
Link
|
Link,
|
||||||
|
ExternalLink,
|
||||||
|
Wind,
|
||||||
|
Plane,
|
||||||
|
Rocket
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -221,67 +222,74 @@ export function WorkspaceDetailDialog() {
|
|||||||
workspaceSecretLink: state.workspaceSecretLink
|
workspaceSecretLink: state.workspaceSecretLink
|
||||||
})))
|
})))
|
||||||
const linkItems: LinkItem[] = [
|
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,
|
key: 'webide' as LinkItemKey,
|
||||||
label: 'Web IDE',
|
label: 'Web IDE',
|
||||||
icon: <Code2 className="w-5 h-5" />,
|
icon: <Code2 className="w-5 h-5" />,
|
||||||
order: 1,
|
order: 2,
|
||||||
getUrl: (data) => data.webide
|
getUrl: (data) => data.webide
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'vscode' as LinkItemKey,
|
key: 'vscode' as LinkItemKey,
|
||||||
label: 'VS Code',
|
label: 'VS Code',
|
||||||
icon: <Code2 className="w-5 h-5" />,
|
icon: <Code2 className="w-5 h-5" />,
|
||||||
order: 2,
|
order: 3,
|
||||||
getUrl: (data) => data.vscode
|
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,
|
key: 'cursor' as LinkItemKey,
|
||||||
label: 'Cursor',
|
label: 'Cursor',
|
||||||
icon: <MousePointer2 className="w-5 h-5" />,
|
icon: <MousePointer2 className="w-5 h-5" />,
|
||||||
order: 6,
|
order: 4,
|
||||||
getUrl: (data) => data.cursor
|
getUrl: (data) => data.cursor
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'jetbrains' as LinkItemKey,
|
key: 'trae-cn' as LinkItemKey,
|
||||||
label: 'JetBrains IDEs',
|
label: 'Trae',
|
||||||
icon: <Box className="w-5 h-5" />,
|
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,
|
order: 7,
|
||||||
getUrl: (data) => Object.values(data.jetbrains || {}).find(Boolean)
|
getUrl: (data) => data.antigravity
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ssh' as LinkItemKey,
|
key: 'ssh' as LinkItemKey,
|
||||||
label: 'SSH',
|
label: 'SSH',
|
||||||
icon: <Lock className="w-5 h-5" />,
|
icon: <Lock className="w-5 h-5" />,
|
||||||
order: 4,
|
order: 9,
|
||||||
getUrl: (data) => data.ssh
|
getUrl: (data) => data.ssh
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'remoteSsh' as LinkItemKey,
|
key: 'remoteSsh' as LinkItemKey,
|
||||||
label: 'Remote SSH',
|
label: 'Remote SSH',
|
||||||
icon: <Radio className="w-5 h-5" />,
|
icon: <Radio className="w-5 h-5" />,
|
||||||
order: 8,
|
order: 10,
|
||||||
getUrl: (data) => data.remoteSsh
|
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,
|
key: 'codebuddycn' as LinkItemKey,
|
||||||
label: 'CodeBuddy CN',
|
label: 'CodeBuddy',
|
||||||
icon: <Zap className="w-5 h-5" />,
|
icon: <Zap className="w-5 h-5" />,
|
||||||
order: 3,
|
order: 11,
|
||||||
getUrl: (data) => data.codebuddycn
|
getUrl: (data) => data.codebuddycn
|
||||||
},
|
},
|
||||||
].sort((a, b) => (a.order || 0) - (b.order || 0))
|
].sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||||
|
|||||||
@@ -24,20 +24,15 @@ export const App = () => {
|
|||||||
setShowCreateDialog: state.setShowCreateDialog,
|
setShowCreateDialog: state.setShowCreateDialog,
|
||||||
})))
|
})))
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [filterDev, setFilterDev] = useState(false)
|
const [filterDev, setFilterDev] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('repos-filter-dev')
|
||||||
|
return saved === 'true'
|
||||||
|
})
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const me = useLayoutStore(state => state.me)
|
|
||||||
const configStore = useConfigStore(useShallow(state => ({ checkConfig: state.checkConfig })))
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh({ showTips: false })
|
refresh({ showTips: false })
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(() => {
|
|
||||||
if (me && me.id) {
|
|
||||||
configStore.checkConfig({ isUser: true, reload: true })
|
|
||||||
}
|
|
||||||
}, [me])
|
|
||||||
|
|
||||||
|
|
||||||
const appList = useMemo(() => {
|
const appList = useMemo(() => {
|
||||||
const sortedList = [...list].sort((a, b) => {
|
const sortedList = [...list].sort((a, b) => {
|
||||||
const aActive = workspaceList.some(ws => ws.slug === a.path)
|
const aActive = workspaceList.some(ws => ws.slug === a.path)
|
||||||
@@ -136,7 +131,11 @@ export const App = () => {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="filter-dev"
|
id="filter-dev"
|
||||||
checked={filterDev}
|
checked={filterDev}
|
||||||
onCheckedChange={(checked) => setFilterDev(checked === true)}
|
onCheckedChange={(checked) => {
|
||||||
|
const value = checked === true
|
||||||
|
setFilterDev(value)
|
||||||
|
localStorage.setItem('repos-filter-dev', String(value))
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="filter-dev"
|
htmlFor="filter-dev"
|
||||||
|
|||||||
@@ -546,16 +546,22 @@ export const useRepoStore = create<State>((set, get) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export type WorkspaceOpen = {
|
export type WorkspaceOpen = {
|
||||||
codebuddy: string;
|
url?: string
|
||||||
codebuddycn: string;
|
webide?: string
|
||||||
cursor: string;
|
jumpUrl?: string
|
||||||
jetbrains: Record<string, string>;
|
remoteSsh?: string
|
||||||
jumpUrl: string;
|
jetbrains?: Record<string, string>
|
||||||
remoteSsh: string;
|
codebuddy?: string
|
||||||
ssh: string;
|
codebuddycn?: string
|
||||||
vscode: string;
|
vscode?: string
|
||||||
'vscode-insiders': string;
|
cursor?: string
|
||||||
webide: 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 openWorkspace = (workspace: WorkspaceInfo, params: { vscode?: boolean, ssh?: boolean }) => {
|
||||||
const openVsCode = params?.vscode ?? true;
|
const openVsCode = params?.vscode ?? true;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FolderKanban, LayoutDashboard, Settings, PlayCircle } from 'lucide-react'
|
import { FolderKanban, LayoutDashboard, Settings, PlayCircle, Cloud } from 'lucide-react'
|
||||||
import { Sidebar, type NavItem } from '@/components/a/Sidebar'
|
import { Sidebar, type NavItem } from '@/components/a/Sidebar'
|
||||||
import { Logo } from './CNBBlackLogo.tsx'
|
import { Logo } from './CNBBlackLogo.tsx'
|
||||||
|
|
||||||
@@ -8,6 +8,11 @@ const navItems: NavItem[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
icon: <FolderKanban className="w-5 h-5" />,
|
icon: <FolderKanban className="w-5 h-5" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '云端环境',
|
||||||
|
path: '/cloud-env',
|
||||||
|
icon: <Cloud className="w-5 h-5" />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '工作空间',
|
title: '工作空间',
|
||||||
path: '/workspaces',
|
path: '/workspaces',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Route as IndexRouteImport } from './routes/index'
|
|||||||
import { Route as WorkspacesIndexRouteImport } from './routes/workspaces/index'
|
import { Route as WorkspacesIndexRouteImport } from './routes/workspaces/index'
|
||||||
import { Route as RepoIndexRouteImport } from './routes/repo/index'
|
import { Route as RepoIndexRouteImport } from './routes/repo/index'
|
||||||
import { Route as ConfigIndexRouteImport } from './routes/config/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'
|
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
|
||||||
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
@@ -47,6 +48,11 @@ const ConfigIndexRoute = ConfigIndexRouteImport.update({
|
|||||||
path: '/config/',
|
path: '/config/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const CloudEnvIndexRoute = CloudEnvIndexRouteImport.update({
|
||||||
|
id: '/cloud-env/',
|
||||||
|
path: '/cloud-env/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ConfigGiteaRoute = ConfigGiteaRouteImport.update({
|
const ConfigGiteaRoute = ConfigGiteaRouteImport.update({
|
||||||
id: '/config/gitea',
|
id: '/config/gitea',
|
||||||
path: '/config/gitea',
|
path: '/config/gitea',
|
||||||
@@ -58,6 +64,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/demo': typeof DemoRoute
|
'/demo': typeof DemoRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/config/gitea': typeof ConfigGiteaRoute
|
'/config/gitea': typeof ConfigGiteaRoute
|
||||||
|
'/cloud-env/': typeof CloudEnvIndexRoute
|
||||||
'/config/': typeof ConfigIndexRoute
|
'/config/': typeof ConfigIndexRoute
|
||||||
'/repo/': typeof RepoIndexRoute
|
'/repo/': typeof RepoIndexRoute
|
||||||
'/workspaces/': typeof WorkspacesIndexRoute
|
'/workspaces/': typeof WorkspacesIndexRoute
|
||||||
@@ -67,6 +74,7 @@ export interface FileRoutesByTo {
|
|||||||
'/demo': typeof DemoRoute
|
'/demo': typeof DemoRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/config/gitea': typeof ConfigGiteaRoute
|
'/config/gitea': typeof ConfigGiteaRoute
|
||||||
|
'/cloud-env': typeof CloudEnvIndexRoute
|
||||||
'/config': typeof ConfigIndexRoute
|
'/config': typeof ConfigIndexRoute
|
||||||
'/repo': typeof RepoIndexRoute
|
'/repo': typeof RepoIndexRoute
|
||||||
'/workspaces': typeof WorkspacesIndexRoute
|
'/workspaces': typeof WorkspacesIndexRoute
|
||||||
@@ -77,6 +85,7 @@ export interface FileRoutesById {
|
|||||||
'/demo': typeof DemoRoute
|
'/demo': typeof DemoRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/config/gitea': typeof ConfigGiteaRoute
|
'/config/gitea': typeof ConfigGiteaRoute
|
||||||
|
'/cloud-env/': typeof CloudEnvIndexRoute
|
||||||
'/config/': typeof ConfigIndexRoute
|
'/config/': typeof ConfigIndexRoute
|
||||||
'/repo/': typeof RepoIndexRoute
|
'/repo/': typeof RepoIndexRoute
|
||||||
'/workspaces/': typeof WorkspacesIndexRoute
|
'/workspaces/': typeof WorkspacesIndexRoute
|
||||||
@@ -88,6 +97,7 @@ export interface FileRouteTypes {
|
|||||||
| '/demo'
|
| '/demo'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/config/gitea'
|
| '/config/gitea'
|
||||||
|
| '/cloud-env/'
|
||||||
| '/config/'
|
| '/config/'
|
||||||
| '/repo/'
|
| '/repo/'
|
||||||
| '/workspaces/'
|
| '/workspaces/'
|
||||||
@@ -97,6 +107,7 @@ export interface FileRouteTypes {
|
|||||||
| '/demo'
|
| '/demo'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/config/gitea'
|
| '/config/gitea'
|
||||||
|
| '/cloud-env'
|
||||||
| '/config'
|
| '/config'
|
||||||
| '/repo'
|
| '/repo'
|
||||||
| '/workspaces'
|
| '/workspaces'
|
||||||
@@ -106,6 +117,7 @@ export interface FileRouteTypes {
|
|||||||
| '/demo'
|
| '/demo'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/config/gitea'
|
| '/config/gitea'
|
||||||
|
| '/cloud-env/'
|
||||||
| '/config/'
|
| '/config/'
|
||||||
| '/repo/'
|
| '/repo/'
|
||||||
| '/workspaces/'
|
| '/workspaces/'
|
||||||
@@ -116,6 +128,7 @@ export interface RootRouteChildren {
|
|||||||
DemoRoute: typeof DemoRoute
|
DemoRoute: typeof DemoRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
ConfigGiteaRoute: typeof ConfigGiteaRoute
|
ConfigGiteaRoute: typeof ConfigGiteaRoute
|
||||||
|
CloudEnvIndexRoute: typeof CloudEnvIndexRoute
|
||||||
ConfigIndexRoute: typeof ConfigIndexRoute
|
ConfigIndexRoute: typeof ConfigIndexRoute
|
||||||
RepoIndexRoute: typeof RepoIndexRoute
|
RepoIndexRoute: typeof RepoIndexRoute
|
||||||
WorkspacesIndexRoute: typeof WorkspacesIndexRoute
|
WorkspacesIndexRoute: typeof WorkspacesIndexRoute
|
||||||
@@ -165,6 +178,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ConfigIndexRouteImport
|
preLoaderRoute: typeof ConfigIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/cloud-env/': {
|
||||||
|
id: '/cloud-env/'
|
||||||
|
path: '/cloud-env'
|
||||||
|
fullPath: '/cloud-env/'
|
||||||
|
preLoaderRoute: typeof CloudEnvIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/config/gitea': {
|
'/config/gitea': {
|
||||||
id: '/config/gitea'
|
id: '/config/gitea'
|
||||||
path: '/config/gitea'
|
path: '/config/gitea'
|
||||||
@@ -180,6 +200,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
DemoRoute: DemoRoute,
|
DemoRoute: DemoRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
ConfigGiteaRoute: ConfigGiteaRoute,
|
ConfigGiteaRoute: ConfigGiteaRoute,
|
||||||
|
CloudEnvIndexRoute: CloudEnvIndexRoute,
|
||||||
ConfigIndexRoute: ConfigIndexRoute,
|
ConfigIndexRoute: ConfigIndexRoute,
|
||||||
RepoIndexRoute: RepoIndexRoute,
|
RepoIndexRoute: RepoIndexRoute,
|
||||||
WorkspacesIndexRoute: WorkspacesIndexRoute,
|
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