generated from kevisual/vite-react-template
feat: 添加创建仓库对话框和停止工作区功能,优化仓库列表界面
This commit is contained in:
128
src/app/repo/modules/CreateRepoDialog.tsx
Normal file
128
src/app/repo/modules/CreateRepoDialog.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,10 +42,19 @@ 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">
|
||||||
|
<div>
|
||||||
<h1 className="text-4xl font-bold text-neutral-900 mb-2">仓库列表</h1>
|
<h1 className="text-4xl font-bold text-neutral-900 mb-2">仓库列表</h1>
|
||||||
<p className="text-neutral-600">共 {list.length} 个仓库</p>
|
<p className="text-neutral-600">共 {list.length} 个仓库</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowCreateDialog(true)}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
新建仓库
|
||||||
|
</Button>
|
||||||
|
</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">
|
||||||
{list.map((repo) => (
|
{list.map((repo) => (
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user