generated from kevisual/vite-react-template
feat: 添加工作区链接功能,更新状态管理以支持获取和显示工作区的秘密链接
This commit is contained in:
@@ -1,10 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipProvider
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
import { useRepoStore } from '../store'
|
import { useRepoStore } from '../store'
|
||||||
import type { WorkspaceOpen } from '../store'
|
import type { WorkspaceOpen } from '../store'
|
||||||
import {
|
import {
|
||||||
@@ -19,11 +24,13 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
Copy,
|
Copy,
|
||||||
Check,
|
Check,
|
||||||
Square
|
Square,
|
||||||
|
Link
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useShallow } from 'zustand/shallow'
|
import { useShallow } from 'zustand/shallow'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type LinkItemKey = keyof WorkspaceOpen;
|
type LinkItemKey = keyof WorkspaceOpen;
|
||||||
interface LinkItem {
|
interface LinkItem {
|
||||||
@@ -35,7 +42,6 @@ interface LinkItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LinkItem = ({ label, icon, url }: { label: string; icon: React.ReactNode; url?: string }) => {
|
const LinkItem = ({ label, icon, url }: { label: string; icon: React.ReactNode; url?: string }) => {
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
|
||||||
const [isCopied, setIsCopied] = useState(false)
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@@ -43,7 +49,7 @@ const LinkItem = ({ label, icon, url }: { label: string; icon: React.ReactNode;
|
|||||||
copy()
|
copy()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (url) {
|
if (url && url.startsWith('http')) {
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,41 +63,38 @@ const LinkItem = ({ label, icon, url }: { label: string; icon: React.ReactNode;
|
|||||||
toast.error('复制失败')
|
toast.error('复制失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleCopy = async (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
if (!url) return
|
|
||||||
copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<TooltipProvider delay={200}>
|
||||||
onClick={handleClick}
|
<Tooltip>
|
||||||
disabled={!url}
|
<TooltipTrigger>
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
<div
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onClick={handleClick}
|
||||||
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"
|
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">
|
<div className="w-8 h-8 flex items-center justify-center text-neutral-700">
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-neutral-900 flex-1 text-left truncate">{label}</span>
|
<span className="text-sm font-medium text-neutral-900 flex-1 text-left truncate">{label}</span>
|
||||||
{url && isHovered && (
|
{url && (
|
||||||
<div
|
<div
|
||||||
onClick={handleCopy}
|
onClick={(e) => {
|
||||||
role="button"
|
e.stopPropagation()
|
||||||
tabIndex={0}
|
copy()
|
||||||
onKeyDown={(e) => {
|
}}
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
role="button"
|
||||||
e.preventDefault()
|
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"
|
||||||
handleCopy(e as any)
|
>
|
||||||
}
|
{isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||||
}}
|
</div>
|
||||||
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"
|
)}
|
||||||
>
|
</div>
|
||||||
{isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
</TooltipTrigger>
|
||||||
</div>
|
<TooltipContent>
|
||||||
)}
|
<p>{url}</p>
|
||||||
</button>
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +127,34 @@ const DevTabContent = ({ linkItems, workspaceLink, stopWorkspace }: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Link tab 内容(暂留空)
|
||||||
|
const LinkTabContent = () => {
|
||||||
|
const store = useRepoStore(useShallow((state) => ({
|
||||||
|
selectWorkspace: state.selectWorkspace,
|
||||||
|
workspaceSecretLink: state.workspaceSecretLink,
|
||||||
|
})))
|
||||||
|
const links = store.workspaceSecretLink.map(item => ({
|
||||||
|
label: item.title,
|
||||||
|
url: item.value
|
||||||
|
}))
|
||||||
|
if (links.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 text-neutral-400">
|
||||||
|
暂无链接, 或工作区未启动
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 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 className="w-5 h-5" />} url={link.url} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Work tab 内容(暂留,需要根据 business_id 做事情)
|
// Work tab 内容(暂留,需要根据 business_id 做事情)
|
||||||
const WorkTabContent = () => {
|
const WorkTabContent = () => {
|
||||||
const store = useRepoStore(useShallow((state) => ({
|
const store = useRepoStore(useShallow((state) => ({
|
||||||
@@ -178,7 +209,7 @@ const WorkTabContent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceDetailDialog() {
|
export function WorkspaceDetailDialog() {
|
||||||
const { showWorkspaceDialog, setShowWorkspaceDialog, workspaceLink, stopWorkspace, workspaceTab, setWorkspaceTab, selectWorkspace } = useRepoStore(useShallow((state) => ({
|
const { showWorkspaceDialog, setShowWorkspaceDialog, workspaceLink, stopWorkspace, workspaceTab, setWorkspaceTab, getWorkspaceSecretLink, selectWorkspace, workspaceSecretLink } = useRepoStore(useShallow((state) => ({
|
||||||
showWorkspaceDialog: state.showWorkspaceDialog,
|
showWorkspaceDialog: state.showWorkspaceDialog,
|
||||||
setShowWorkspaceDialog: state.setShowWorkspaceDialog,
|
setShowWorkspaceDialog: state.setShowWorkspaceDialog,
|
||||||
workspaceLink: state.workspaceLink,
|
workspaceLink: state.workspaceLink,
|
||||||
@@ -186,6 +217,8 @@ export function WorkspaceDetailDialog() {
|
|||||||
workspaceTab: state.workspaceTab,
|
workspaceTab: state.workspaceTab,
|
||||||
setWorkspaceTab: state.setWorkspaceTab,
|
setWorkspaceTab: state.setWorkspaceTab,
|
||||||
selectWorkspace: state.selectWorkspace,
|
selectWorkspace: state.selectWorkspace,
|
||||||
|
getWorkspaceSecretLink: state.getWorkspaceSecretLink,
|
||||||
|
workspaceSecretLink: state.workspaceSecretLink
|
||||||
})))
|
})))
|
||||||
const linkItems: LinkItem[] = [
|
const linkItems: LinkItem[] = [
|
||||||
{
|
{
|
||||||
@@ -252,6 +285,11 @@ export function WorkspaceDetailDialog() {
|
|||||||
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))
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectWorkspace) {
|
||||||
|
getWorkspaceSecretLink(selectWorkspace)
|
||||||
|
}
|
||||||
|
}, [selectWorkspace])
|
||||||
return (
|
return (
|
||||||
<Dialog open={showWorkspaceDialog} onOpenChange={setShowWorkspaceDialog}>
|
<Dialog open={showWorkspaceDialog} onOpenChange={setShowWorkspaceDialog}>
|
||||||
<DialogContent className="max-w-md! bg-white">
|
<DialogContent className="max-w-md! bg-white">
|
||||||
@@ -284,12 +322,29 @@ export function WorkspaceDetailDialog() {
|
|||||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setWorkspaceTab('link')}
|
||||||
|
className={clsx(`cursor-pointer flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${workspaceTab === 'link'
|
||||||
|
? 'text-neutral-900'
|
||||||
|
: 'text-neutral-500 hover:text-neutral-700'
|
||||||
|
}`)}
|
||||||
|
>
|
||||||
|
<Link className="w-4 h-4 inline-block mr-1" />
|
||||||
|
Link
|
||||||
|
{workspaceTab === 'link' && (
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* Tab 内容 */}
|
{/* Tab 内容 */}
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
{workspaceTab === 'dev' ? (
|
{workspaceTab === 'dev' && (
|
||||||
<DevTabContent linkItems={linkItems} workspaceLink={workspaceLink} stopWorkspace={stopWorkspace} />
|
<DevTabContent linkItems={linkItems} workspaceLink={workspaceLink} stopWorkspace={stopWorkspace} />
|
||||||
) : (
|
)}
|
||||||
|
{workspaceTab === 'link' && (
|
||||||
|
<LinkTabContent />
|
||||||
|
)}
|
||||||
|
{workspaceTab === 'work' && (
|
||||||
<WorkTabContent />
|
<WorkTabContent />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { queryApi as cnbApi } from '@/modules/cnb-api'
|
|||||||
import { WorkspaceInfo } from '@kevisual/cnb'
|
import { WorkspaceInfo } from '@kevisual/cnb'
|
||||||
import { createBuildConfig, createCommitBlankConfig, createDevConfig } from './build';
|
import { createBuildConfig, createCommitBlankConfig, createDevConfig } from './build';
|
||||||
import { useLayoutStore } from '@/pages/auth/store';
|
import { useLayoutStore } from '@/pages/auth/store';
|
||||||
|
import { Query } from '@kevisual/query';
|
||||||
interface DisplayModule {
|
interface DisplayModule {
|
||||||
activity: boolean;
|
activity: boolean;
|
||||||
contributors: boolean;
|
contributors: boolean;
|
||||||
@@ -51,7 +52,7 @@ interface Data {
|
|||||||
pinned_time: string;
|
pinned_time: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceTabType = 'dev' | 'work'
|
type WorkspaceTabType = 'dev' | 'work' | 'link'
|
||||||
|
|
||||||
type BuildConfig = {
|
type BuildConfig = {
|
||||||
repo: string;
|
repo: string;
|
||||||
@@ -101,6 +102,8 @@ type State = {
|
|||||||
deleteBuildConfig: (params: { repo: Data, user?: any }) => Promise<any>;
|
deleteBuildConfig: (params: { repo: Data, user?: any }) => Promise<any>;
|
||||||
initBuildConfig: (params: { repo: Data, user?: any }) => Promise<any>;
|
initBuildConfig: (params: { repo: Data, user?: any }) => Promise<any>;
|
||||||
buildWorkspace: () => Promise<any>;
|
buildWorkspace: () => Promise<any>;
|
||||||
|
workspaceSecretLink: { title: string, key: string, value?: string }[];
|
||||||
|
getWorkspaceSecretLink: (workspace: WorkspaceInfo) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRepoStore = create<State>((set, get) => {
|
export const useRepoStore = create<State>((set, get) => {
|
||||||
@@ -248,6 +251,50 @@ export const useRepoStore = create<State>((set, get) => {
|
|||||||
toast.error(res.message || '构建触发失败')
|
toast.error(res.message || '构建触发失败')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
workspaceSecretLink: [],
|
||||||
|
getWorkspaceSecretLink: async (workspace) => {
|
||||||
|
console.log('获取工作区链接', workspace)
|
||||||
|
const business_id = workspace?.business_id;
|
||||||
|
const baseURL = `https://${business_id}-51515.cnb.run/client/router`;
|
||||||
|
console.log('工作区链接', baseURL)
|
||||||
|
const url = new URL(baseURL);
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
// const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZC1rZXktMSJ9.eyJzdWIiOiJ1c2VyOjBlNzAwZGM4LTkwZGQtNDFiNy05MWRkLTMzNmVhNTFkZTNkMiIsIm5hbWUiOiJyb290IiwiZXhwIjoxNzczMTI4NTMwLCJpc3MiOiJodHRwczovL2NvbnZleC5rZXZpc3VhbC5jbiIsImlhdCI6MTc3MzEyMTMzMCwiYXVkIjoiY29udmV4LWFwcCJ9.g4kANiPc352QFBfa0yb4gl98mLHTruL_3HvIaKYwN1Qy3-P8QV6X_WhqgMOskQphNGsBFC-LRmZq2808GnqwpjDTE0ekXbsO4L9C-D6F3mBMwowqpvmURCRVg6Ys6LSkzw4sM75VbHpfFX3ZQVtZymvAWhxxxvjhdKGPdrdw5bNymTbCw-Y9NrYW6u2mExLrvrfXl3vJqaCz7obj_mR-G_2PB3g5KPQYhWCl8--TkYOS9fiNIYlcacnO36bZXhHheHFZEr_gb8UG5ECg0ND8hsH8TijiYBAY6T93nhGrZG7E0oQY3xXsVm-mkvXP2tLCXwKH7SFmH4M0tdZLRqLqKw'
|
||||||
|
url.searchParams.set('path', 'cnb_board');
|
||||||
|
url.searchParams.set('key', 'live');
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
}
|
||||||
|
}).then(res => res.json());
|
||||||
|
const labelData: { title: string, key: string, value?: string }[] = [
|
||||||
|
{
|
||||||
|
title: 'Opencode Secret',
|
||||||
|
key: 'opencodeUrlSecret',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Openclaw Secret',
|
||||||
|
key: 'openclawUrlSecret',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'StartTime',
|
||||||
|
key: 'buildStartTime',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (res.code === 200) {
|
||||||
|
const list = res.data?.list || [];
|
||||||
|
const workspaceSecretLink: { title: string, key: string, value?: string }[] = [];
|
||||||
|
labelData.forEach(item => {
|
||||||
|
const find = list.find((l: any) => l.key === item.key);
|
||||||
|
if (find) {
|
||||||
|
workspaceSecretLink.push({ ...item, value: find.value });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
set({ workspaceSecretLink })
|
||||||
|
}
|
||||||
|
},
|
||||||
getItem: async (repo: string) => {
|
getItem: async (repo: string) => {
|
||||||
const { setLoading } = get();
|
const { setLoading } = get();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user