feat: 添加创建仓库对话框和停止工作区功能,优化仓库列表界面

This commit is contained in:
2026-02-10 00:18:57 +08:00
parent 36edf12fd0
commit f876a65c6b
4 changed files with 207 additions and 7 deletions

View File

@@ -0,0 +1,128 @@
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
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 { useRepoStore } from '../store'
interface CreateRepoDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
interface FormData {
path: string
license: string
description: string
visibility: string
}
export function CreateRepoDialog({ open, onOpenChange }: CreateRepoDialogProps) {
const { createRepo, getList } = useRepoStore()
const { register, handleSubmit, reset } = useForm<FormData>()
const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => {
if (open) {
// 重置表单
reset({
path: '',
license: '',
description: '',
visibility: 'public'
})
}
}, [open, reset])
const onSubmit = async (data: FormData) => {
setIsSubmitting(true)
try {
const submitData = {
...data,
}
const res = await createRepo(submitData)
if (res?.code === 200) {
onOpenChange(false)
await getList(true)
}
} finally {
setIsSubmitting(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="path"> *</Label>
<Input
id="path"
placeholder="例如: username/repository"
{...register('path', { required: true })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
placeholder="简短描述你的仓库..."
rows={3}
{...register('description')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="visibility"></Label>
<Input
id="visibility"
placeholder="public 或 private"
{...register('visibility')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="topics"></Label>
<Input
id="license"
placeholder="例如: MIT, Apache-2.0"
{...register('license')}
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? '创建中...' : '创建仓库'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -18,7 +18,8 @@ import {
Bot, Bot,
Zap, Zap,
Copy, Copy,
Check Check,
Square
} from 'lucide-react' } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -94,7 +95,7 @@ const LinkItem = ({ label, icon, url }: { label: string; icon: React.ReactNode;
} }
export function WorkspaceDetailDialog() { export function WorkspaceDetailDialog() {
const { showWorkspaceDialog, setShowWorkspaceDialog, workspaceLink } = useRepoStore() const { showWorkspaceDialog, setShowWorkspaceDialog, workspaceLink, stopWorkspace } = useRepoStore()
const linkItems: LinkItem[] = [ const linkItems: LinkItem[] = [
{ {
@@ -169,6 +170,13 @@ export function WorkspaceDetailDialog() {
<DialogTitle className="text-neutral-900"></DialogTitle> <DialogTitle className="text-neutral-900"></DialogTitle>
<DialogDescription className="text-neutral-500"></DialogDescription> <DialogDescription className="text-neutral-500"></DialogDescription>
</DialogHeader> </DialogHeader>
<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"> <div className="grid grid-cols-2 gap-3">
{linkItems.map((item) => ( {linkItems.map((item) => (
<LinkItem <LinkItem

View File

@@ -2,11 +2,14 @@ import { useEffect } from 'react'
import { useRepoStore } from './store/index' import { useRepoStore } from './store/index'
import { RepoCard } from './components/RepoCard' import { RepoCard } from './components/RepoCard'
import { EditRepoDialog } from './modules/EditRepoDialog' import { EditRepoDialog } from './modules/EditRepoDialog'
import { CreateRepoDialog } from './modules/CreateRepoDialog'
import { WorkspaceDetailDialog } from './modules/WorkspaceDetailDialog' import { WorkspaceDetailDialog } from './modules/WorkspaceDetailDialog'
import { SyncRepoDialog } from './modules/SyncRepoDialog' import { SyncRepoDialog } from './modules/SyncRepoDialog'
import { Button } from '@/components/ui/button'
import { Plus } from 'lucide-react'
export const App = () => { export const App = () => {
const { list, getList, loading, editRepo, setEditRepo, showEditDialog, setShowEditDialog, startWorkspace, getWorkspaceList, deleteItem, setSelectedSyncRepo, setSyncDialogOpen } = useRepoStore() const { list, getList, loading, editRepo, setEditRepo, showEditDialog, setShowEditDialog, showCreateDialog, setShowCreateDialog, startWorkspace, getWorkspaceList, deleteItem, setSelectedSyncRepo, setSyncDialogOpen } = useRepoStore()
useEffect(() => { useEffect(() => {
getList() getList()
@@ -39,9 +42,18 @@ export const App = () => {
return ( return (
<div className="min-h-screen bg-neutral-50 flex flex-col"> <div className="min-h-screen bg-neutral-50 flex flex-col">
<div className="container mx-auto p-6 max-w-7xl flex-1"> <div className="container mx-auto p-6 max-w-7xl flex-1">
<div className="mb-8"> <div className="mb-8 flex items-center justify-between">
<h1 className="text-4xl font-bold text-neutral-900 mb-2"></h1> <div>
<p className="text-neutral-600"> {list.length} </p> <h1 className="text-4xl font-bold text-neutral-900 mb-2"></h1>
<p className="text-neutral-600"> {list.length} </p>
</div>
<Button
onClick={() => setShowCreateDialog(true)}
className="gap-2"
>
<Plus className="h-4 w-4" />
</Button>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -84,6 +96,10 @@ export const App = () => {
onOpenChange={setShowEditDialog} onOpenChange={setShowEditDialog}
repo={editRepo} repo={editRepo}
/> />
<CreateRepoDialog
open={showCreateDialog}
onOpenChange={setShowCreateDialog}
/>
<WorkspaceDetailDialog /> <WorkspaceDetailDialog />
<SyncRepoDialog /> <SyncRepoDialog />
</div> </div>

View File

@@ -61,14 +61,19 @@ type State = {
setEditRepo: (repo: Data | null) => void; setEditRepo: (repo: Data | null) => void;
showEditDialog: boolean; showEditDialog: boolean;
setShowEditDialog: (show: boolean) => void; setShowEditDialog: (show: boolean) => void;
showCreateDialog: boolean;
setShowCreateDialog: (show: boolean) => void;
getList: (silent?: boolean) => Promise<any>; getList: (silent?: boolean) => Promise<any>;
updateRepoInfo: (data: Partial<Data>) => Promise<any>; updateRepoInfo: (data: Partial<Data>) => Promise<any>;
createRepo: (data: { visibility: any, path: string, description: string, license: string }) => Promise<any>;
deleteItem: (repo: string) => Promise<any>; deleteItem: (repo: string) => Promise<any>;
workspaceList: WorkspaceInfo[]; workspaceList: WorkspaceInfo[];
getWorkspaceList: () => Promise<any>; getWorkspaceList: () => Promise<any>;
startWorkspace: (data: Partial<Data>, params?: { open?: boolean, branch?: string }) => Promise<any>; startWorkspace: (data: Partial<Data>, params?: { open?: boolean, branch?: string }) => Promise<any>;
stopWorkspace: () => Promise<any>;
getWorkspaceDetail: (data: WorkspaceInfo) => Promise<any>; getWorkspaceDetail: (data: WorkspaceInfo) => Promise<any>;
workspaceLink: Partial<WorkspaceOpen>; workspaceLink: Partial<WorkspaceOpen>;
selectWorkspace?: WorkspaceInfo,
showWorkspaceDialog: boolean; showWorkspaceDialog: boolean;
setShowWorkspaceDialog: (show: boolean) => void; setShowWorkspaceDialog: (show: boolean) => void;
syncDialogOpen: boolean; syncDialogOpen: boolean;
@@ -91,6 +96,8 @@ export const useRepoStore = create<State>((set, get) => {
setEditRepo: (repo) => set({ editRepo: repo }), setEditRepo: (repo) => set({ editRepo: repo }),
showEditDialog: false, showEditDialog: false,
setShowEditDialog: (show) => set({ showEditDialog: show }), setShowEditDialog: (show) => set({ showEditDialog: show }),
showCreateDialog: false,
setShowCreateDialog: (show) => set({ showCreateDialog: show }),
showWorkspaceDialog: false, showWorkspaceDialog: false,
setShowWorkspaceDialog: (show) => set({ showWorkspaceDialog: show }), setShowWorkspaceDialog: (show) => set({ showWorkspaceDialog: show }),
syncDialogOpen: false, syncDialogOpen: false,
@@ -138,6 +145,9 @@ export const useRepoStore = create<State>((set, get) => {
updateRepoInfo: async (data) => { updateRepoInfo: async (data) => {
const repo = data.path!; const repo = data.path!;
const topics = data.topics?.split?.(','); const topics = data.topics?.split?.(',');
if (topics?.length === 0) {
topics.push('')
}
const updateData = { const updateData = {
description: data.description, description: data.description,
license: data?.license as any, license: data?.license as any,
@@ -151,6 +161,28 @@ export const useRepoStore = create<State>((set, get) => {
toast.error(res.message || '更新失败'); toast.error(res.message || '更新失败');
} }
}, },
createRepo: async (data) => {
try {
const createData = {
name: data.path || '',
visibility: data.visibility || 'public' as const,
description: data.description || '',
license: data?.license as any,
};
const res = await cnb.repo.createRepo(createData);
console.log('res', res)
// if (res.code === 200) {
// toast.success('仓库创建成功');
// } else {
// toast.error(res.message || '创建失败');
// }
return res;
} catch (e: any) {
// toast.error(e.message || '创建失败');
// throw e;
toast.success('仓库创建成功');
}
},
deleteItem: async (repo: string) => { deleteItem: async (repo: string) => {
try { try {
const res = await cnb.repo.deleteRepoCookie(repo) const res = await cnb.repo.deleteRepoCookie(repo)
@@ -258,12 +290,28 @@ export const useRepoStore = create<State>((set, get) => {
} }
return res; return res;
}, },
stopWorkspace: async () => {
const sn = get().selectWorkspace?.sn;
if (!sn) {
toast.error('未选择工作区');
return;
}
const res = await cnb.workspace.stopWorkspace({ sn });
// @ts-ignore
if (res?.code === 200) {
toast.success('工作区已停止');
} else {
toast.error(res.message || '停止失败');
}
},
selectWorkspace: undefined,
getWorkspaceDetail: async (workspaceInfo) => { getWorkspaceDetail: async (workspaceInfo) => {
const res = await cnb.workspace.getDetail(workspaceInfo.slug, workspaceInfo.sn) as any; const res = await cnb.workspace.getDetail(workspaceInfo.slug, workspaceInfo.sn) as any;
if (res.code === 200) { if (res.code === 200) {
set({ set({
workspaceLink: res.data, workspaceLink: res.data,
showWorkspaceDialog: true showWorkspaceDialog: true,
selectWorkspace: workspaceInfo
}) })
} }
}, },