generated from kevisual/vite-react-template
update
This commit is contained in:
285
src/pages/repos/modules/WorkspaceDetailDialog.tsx
Normal file
285
src/pages/repos/modules/WorkspaceDetailDialog.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { useRepoStore } from '../store'
|
||||
import type { WorkspaceOpen } from '../store'
|
||||
import {
|
||||
Code2,
|
||||
Terminal,
|
||||
Sparkles,
|
||||
MousePointer2,
|
||||
Box,
|
||||
Lock,
|
||||
Radio,
|
||||
Bot,
|
||||
Zap,
|
||||
Copy,
|
||||
Check,
|
||||
Square
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useShallow } from 'zustand/shallow'
|
||||
|
||||
type LinkItemKey = keyof WorkspaceOpen;
|
||||
interface LinkItem {
|
||||
key: LinkItemKey
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
order?: number
|
||||
getUrl: (data: Partial<WorkspaceOpen>) => string | undefined
|
||||
}
|
||||
|
||||
const LinkItem = ({ label, icon, url }: { label: string; icon: React.ReactNode; url?: string }) => {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
if (url?.startsWith?.('ssh') || url?.startsWith?.('cnb')) {
|
||||
copy()
|
||||
return;
|
||||
}
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url!)
|
||||
setIsCopied(true)
|
||||
toast.success('已复制到剪贴板')
|
||||
setTimeout(() => setIsCopied(false), 2000)
|
||||
} catch (error) {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!url) return
|
||||
copy()
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={!url}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className="relative flex items-center gap-3 p-3 rounded-lg border border-neutral-200 hover:border-neutral-900 hover:bg-neutral-50 transition-all disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-neutral-200 disabled:hover:bg-transparent group"
|
||||
>
|
||||
<div className="w-8 h-8 flex items-center justify-center text-neutral-700">
|
||||
{icon}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-neutral-900 flex-1 text-left truncate">{label}</span>
|
||||
{url && isHovered && (
|
||||
<div
|
||||
onClick={handleCopy}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleCopy(e as any)
|
||||
}
|
||||
}}
|
||||
className="w-6 h-6 flex items-center justify-center text-neutral-500 hover:text-neutral-900 hover:bg-neutral-100 rounded transition-colors cursor-pointer"
|
||||
>
|
||||
{isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Dev tab 内容
|
||||
const DevTabContent = ({ linkItems, workspaceLink, stopWorkspace }: {
|
||||
linkItems: LinkItem[]
|
||||
workspaceLink: Partial<WorkspaceOpen>
|
||||
stopWorkspace: () => void
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => stopWorkspace()}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg bg-red-500 hover:bg-red-600 text-white font-medium transition-colors"
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
停止工作区
|
||||
</button>
|
||||
<div className="grid grid-cols-2 gap-3 mt-2">
|
||||
{linkItems.map((item) => (
|
||||
<LinkItem
|
||||
key={item.key}
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
url={item.getUrl(workspaceLink)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Work tab 内容(暂留,需要根据 business_id 做事情)
|
||||
const WorkTabContent = () => {
|
||||
const store = useRepoStore(useShallow((state) => ({ selectWorkspace: state.selectWorkspace })))
|
||||
const businessId = store.selectWorkspace?.business_id;
|
||||
|
||||
const appList = [
|
||||
{
|
||||
title: 'Kevisual Assistant Client', key: 'Assistant Client', port: 51515, end: '/root/cli-center/'
|
||||
},
|
||||
{
|
||||
title: 'OpenCode', key: 'OpenCode', port: 100, end: ''
|
||||
},
|
||||
{
|
||||
title: 'OpenClaw', key: 'OpenClaw', port: 80, end: '/openclaw'
|
||||
},
|
||||
{
|
||||
title: 'OpenWebUI', key: 'OpenWebUI', port: 200, end: ''
|
||||
},
|
||||
]
|
||||
const links = appList.map(app => {
|
||||
const url = `https://${businessId}-${app.port}.cnb.run${app.end}`
|
||||
return {
|
||||
label: app.title,
|
||||
icon: <Terminal className="w-5 h-5" />,
|
||||
url
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-2 text-neutral-400">
|
||||
<div className="grid grid-cols-1 gap-3 w-full max-w-sm">
|
||||
{links.map(link => (
|
||||
<LinkItem key={link.label} label={link.label} icon={link.icon} url={link.url} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkspaceDetailDialog() {
|
||||
const { showWorkspaceDialog, setShowWorkspaceDialog, workspaceLink, stopWorkspace, workspaceTab, setWorkspaceTab, selectWorkspace } = useRepoStore(useShallow((state) => ({
|
||||
showWorkspaceDialog: state.showWorkspaceDialog,
|
||||
setShowWorkspaceDialog: state.setShowWorkspaceDialog,
|
||||
workspaceLink: state.workspaceLink,
|
||||
stopWorkspace: state.stopWorkspace,
|
||||
workspaceTab: state.workspaceTab,
|
||||
setWorkspaceTab: state.setWorkspaceTab,
|
||||
selectWorkspace: state.selectWorkspace,
|
||||
})))
|
||||
const linkItems: LinkItem[] = [
|
||||
{
|
||||
key: 'webide' as LinkItemKey,
|
||||
label: 'Web IDE',
|
||||
icon: <Code2 className="w-5 h-5" />,
|
||||
order: 1,
|
||||
getUrl: (data) => data.webide
|
||||
},
|
||||
{
|
||||
key: 'vscode' as LinkItemKey,
|
||||
label: 'VS Code',
|
||||
icon: <Code2 className="w-5 h-5" />,
|
||||
order: 2,
|
||||
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,
|
||||
getUrl: (data) => data.cursor
|
||||
},
|
||||
{
|
||||
key: 'jetbrains' as LinkItemKey,
|
||||
label: 'JetBrains IDEs',
|
||||
icon: <Box className="w-5 h-5" />,
|
||||
order: 7,
|
||||
getUrl: (data) => Object.values(data.jetbrains || {}).find(Boolean)
|
||||
},
|
||||
{
|
||||
key: 'ssh' as LinkItemKey,
|
||||
label: 'SSH',
|
||||
icon: <Lock className="w-5 h-5" />,
|
||||
order: 4,
|
||||
getUrl: (data) => data.ssh
|
||||
},
|
||||
{
|
||||
key: 'remoteSsh' as LinkItemKey,
|
||||
label: 'Remote SSH',
|
||||
icon: <Radio className="w-5 h-5" />,
|
||||
order: 8,
|
||||
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',
|
||||
icon: <Zap className="w-5 h-5" />,
|
||||
order: 3,
|
||||
getUrl: (data) => data.codebuddycn
|
||||
},
|
||||
].sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
console.log('workspaceLink', selectWorkspace)
|
||||
return (
|
||||
<Dialog open={showWorkspaceDialog} onOpenChange={setShowWorkspaceDialog}>
|
||||
<DialogContent className="max-w-md! bg-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-neutral-900">工作区</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* Tab 导航 */}
|
||||
<div className="flex border-b border-neutral-200">
|
||||
<button
|
||||
onClick={() => setWorkspaceTab('dev')}
|
||||
className={`cursor-pointer flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${workspaceTab === 'dev'
|
||||
? 'text-neutral-900'
|
||||
: 'text-neutral-500 hover:text-neutral-700'
|
||||
}`}
|
||||
>
|
||||
Dev
|
||||
{workspaceTab === 'dev' && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWorkspaceTab('work')}
|
||||
className={`cursor-pointer flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${workspaceTab === 'work'
|
||||
? 'text-neutral-900'
|
||||
: 'text-neutral-500 hover:text-neutral-700'
|
||||
}`}
|
||||
>
|
||||
Work
|
||||
{workspaceTab === 'work' && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{/* Tab 内容 */}
|
||||
<div className="py-2">
|
||||
{workspaceTab === 'dev' ? (
|
||||
<DevTabContent linkItems={linkItems} workspaceLink={workspaceLink} stopWorkspace={stopWorkspace} />
|
||||
) : (
|
||||
<WorkTabContent />
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user