feat: enhance repository management UI and functionality

- Added navigation to repository details in RepoCard component.
- Implemented a new BuildConfig component for managing build configurations.
- Integrated build configuration initialization and saving logic in the store.
- Updated RepoInfoCard to include workspace management features.
- Improved repository editing dialog with better state handling.
- Enhanced repository list fetching with search capabilities.
- Added support for creating and managing development configurations.
- Refactored code for better readability and maintainability.
This commit is contained in:
2026-02-26 01:11:45 +08:00
parent 5a769a6748
commit a54597c65e
10 changed files with 785 additions and 65 deletions

View File

@@ -0,0 +1,130 @@
import { useEffect, useState } from "react";
import { useRepoStore } from "../store";
import { useShallow } from "zustand/shallow";
import { toast } from "sonner";
import CodeMirror from "@uiw/react-codemirror";
import { yaml } from "@codemirror/lang-yaml";
import { useLayoutStore } from "@/pages/auth/store";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Workflow } from "lucide-react";
export const BuildConfig = () => {
const repoStore = useRepoStore(useShallow((state) => ({
getItem: state.getItem,
editRepo: state.editRepo,
buildConfig: state.buildConfig,
setBuildConfig: state.setBuildConfig,
initBuildConfig: state.initBuildConfig,
deleteBuildConfig: state.deleteBuildConfig,
loading: state.loading,
buildWorkspace: state.buildWorkspace,
})));
const repo = repoStore.editRepo!;
const me = useLayoutStore((state) => state.me);
const [localConfig, setLocalConfig] = useState(repoStore.buildConfig?.config || "");
// 同步 buildConfig 变化时的状态
useEffect(() => {
setLocalConfig(repoStore.buildConfig?.config || "");
}, [repoStore.buildConfig]);
useEffect(() => {
if (repo) {
repoStore.initBuildConfig({ repo: repo, user: me });
}
}, [repo, me])
const handleSave = () => {
if (repoStore.buildConfig) {
repoStore.setBuildConfig({
...repoStore.buildConfig,
config: localConfig,
}, true);
}
};
const handleFieldChange = (field: string, value: string | null) => {
if (repoStore.buildConfig && value !== null) {
repoStore.setBuildConfig({
...repoStore.buildConfig,
[field]: value,
}, false);
}
};
if (repoStore.loading) {
return <div>Loading...</div>
}
return (
<div className="flex gap-4 h-full overflow-hidden">
{/* 左侧边栏 - 配置信息 */}
<div className="w-64 shrink-0 space-y-4">
<div className="text-xl font-bold border-b pb-2 mb-4 flex">
<span className="text-lg font-semibold"></span>
<button
onClick={repoStore.buildWorkspace}
className="ml-auto p-2 text-sm cursor-pointer bg-gray-500 text-white rounded hover:bg-gray-600 flex items-center"
title="构建工作空间"
>
<Workflow className="w-4 h-4" />
</button>
</div>
<div className="space-y-2">
<label className="text-sm text-neutral-500"></label>
<Input
value={repoStore.buildConfig?.repo || ""}
onChange={(e) => handleFieldChange("repo", e.target.value)}
placeholder="仓库名称"
/>
</div>
<div className="space-y-2">
<label className="text-sm text-neutral-500"></label>
<Input
value={repoStore.buildConfig?.branch || ""}
onChange={(e) => handleFieldChange("branch", e.target.value)}
placeholder="分支名称"
/>
</div>
<div className="space-y-2">
<label className="text-sm text-neutral-500"></label>
<Input
value={repoStore.buildConfig?.event || ""}
onChange={(e) => handleFieldChange("event", e.target.value)}
placeholder="事件名称"
/>
</div>
</div>
{/* 右侧 - 编辑器 */}
<div className="flex-1 flex flex-col h-full ">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium"></span>
<div className="flex gap-2">
<button
onClick={handleSave}
className="px-3 cursor-pointer py-1 text-sm bg-primary text-white rounded hover:bg-primary/90"
>
</button>
<button
onClick={() => repoStore.deleteBuildConfig({ repo: repo, user: me })}
className="px-3 cursor-pointer py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600"
>
</button>
</div>
</div>
<div className="border rounded-md flex-1 h-[calc(100%-40px)] overflow-auto scrollbar">
<CodeMirror
value={localConfig}
height="100%"
extensions={[yaml()]}
onChange={(value) => setLocalConfig(value)}
theme="light"
/>
</div>
</div>
</div>
)
}
export default BuildConfig;

View File

@@ -13,13 +13,14 @@ import {
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square } from 'lucide-react'
import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square, LinkIcon, ExternalLink } from 'lucide-react'
import { useRepoStore } from '../store'
import { useMemo, useState } from 'react'
import { useShallow } from 'zustand/shallow'
import { myOrgs } from '../store/build'
import { app, cnb } from '@/agents/app'
import { toast } from 'sonner'
import { useNavigate } from '@tanstack/react-router'
interface RepoCardProps {
repo: any
@@ -46,13 +47,13 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
const isWorkspaceActive = !!workspace
const owner = repo.path.split('/')[0]
const isMine = myOrgs.includes(owner)
const navigate = useNavigate();
const isKnowledge = repo?.flags === "KnowledgeBase"
const createKnow = async () => {
const res = await app.run({ path: 'cnb', key: 'build-knowledge-base', payload: { repo: repo.path } })
if (res.code === 200) {
toast.success("知识库创建中")
getList(true)
getList({}, true)
}
}
const onClone = async () => {
@@ -72,6 +73,14 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
<div className="p-6 space-y-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div
className="text-lg font-bold text-neutral-900 hover:text-neutral-600 transition-colors line-clamp-1 group-hover:underline"
onClick={() => {
navigate({ to: `/repo?repo=${repo.path}` })
}}
>
{repo.path}
</div>
{isKnowledge && (
<TooltipProvider>
<Tooltip>
@@ -88,14 +97,6 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
</Tooltip>
</TooltipProvider>
)}
<a
href={repo.web_url}
target="_blank"
rel="noopener noreferrer"
className="text-lg font-bold text-neutral-900 hover:text-neutral-600 transition-colors line-clamp-1 group-hover:underline"
>
{repo.path}
</a>
{isWorkspaceActive && (
<span className="relative flex h-2.5 w-2.5 shrink-0">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
@@ -195,13 +196,19 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
<Copy className="w-4 h-4 mr-2" />
Clone URL
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {
window.open(repo.web_url, '_blank')
}} className="cursor-pointer">
<ExternalLink className="w-4 h-4 mr-2" />
访
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onIssue(repo)} className="cursor-pointer">
<IssueIcon className="w-4 h-4 mr-2" />
Issue
访
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onSettings(repo)} className="cursor-pointer">
<Settings className="w-4 h-4 mr-2" />
访
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
@@ -268,22 +275,25 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
</div>
{repo.site && (
<a
href={repo.site}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-neutral-500 hover:text-neutral-900 hover:underline block truncate transition-colors"
<div
className="text-xs text-neutral-500 hover:text-neutral-900 hover:underline flex transition-colors"
onClick={() => {
window.open(repo.site, '_blank')
}}
>
🔗 {repo.site}
</a>
<LinkIcon className="w-4 h-4 shrink-0 mr-2" />
<div className='truncate grow'>
{repo.site}
</div>
</div>
)}
{repo.description && (
<p className="text-sm text-neutral-600 line-clamp-2 min-h-10">
<p className="ml-2 text-sm text-neutral-600 line-clamp-2 min-h-10 grow">
{repo.description}
</p>
)}
<div className="absolute bottom-0 left-0 right-0 flex items-center gap-4 text-xs text-neutral-500 px-6 py-3 border-t border-neutral-100 bg-neutral-50">
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
<Star className="w-3.5 h-3.5" />

View File

@@ -1,16 +1,35 @@
import { useNavigate } from "@tanstack/react-router";
import { useMemo } from "react";
import { useRepoStore } from "../store";
import { useShallow } from "zustand/shallow";
import { toast } from "sonner";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Star, GitFork, FileText, ExternalLink, Calendar, User, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Star, GitFork, FileText, ExternalLink, Calendar, User, Copy, ArrowLeft, Play, Square, Eye, BookOpen, RefreshCw } from "lucide-react";
import { myOrgs } from "../store/build";
export const RepoInfoCard = () => {
const repoStore = useRepoStore(useShallow((state) => ({
getItem: state.getItem,
const navigate = useNavigate();
const { workspaceList, getWorkspaceDetail, stopWorkspace, editRepo, setSelectedSyncRepo, setSyncDialogOpen } = useRepoStore(useShallow((state) => ({
workspaceList: state.workspaceList,
getWorkspaceDetail: state.getWorkspaceDetail,
stopWorkspace: state.stopWorkspace,
editRepo: state.editRepo,
setSelectedSyncRepo: state.setSelectedSyncRepo,
setSyncDialogOpen: state.setSyncDialogOpen,
})));
const repo = repoStore.editRepo!;
const repo = editRepo!;
const workspace = useMemo(() => {
return workspaceList.find(ws => ws.slug === repo.path)
}, [workspaceList, repo.path])
const isWorkspaceActive = !!workspace
const owner = repo.path.split('/')[0]
const isMine = myOrgs.includes(owner)
const isKnowledge = repo?.flags === "KnowledgeBase"
const onClone = () => {
const url = `git clone https://cnb.cool/${repo.path}`
navigator.clipboard.writeText(url).then(() => {
@@ -30,6 +49,13 @@ export const RepoInfoCard = () => {
{/* 标题行 */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<button
onClick={() => navigate({ to: '/' })}
className="cursor-pointer flex items-center justify-center w-8 h-8 rounded-md hover:bg-neutral-100 transition-colors"
>
<ArrowLeft className="w-4 h-4 text-neutral-600" />
</button>
<span className="text-sm text-neutral-500 font-mono">
{repo.path}
</span>
@@ -43,37 +69,147 @@ export const RepoInfoCard = () => {
<Badge variant="outline" className="shrink-0">
{repo.visibility_level === 'Public' ? '公开' : repo.visibility_level === 'Private' ? '私有' : repo.visibility_level}
</Badge>
{isKnowledge && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<div className="shrink-0">
<BookOpen className="w-5 h-5 text-neutral-700" />
</div>
}
/>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isWorkspaceActive && (
<span className="relative flex h-2.5 w-2.5 shrink-0">
<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>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
{isWorkspaceActive && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<Button
size="sm"
variant="outline"
onClick={() => {
stopWorkspace(workspace)
}}
className="h-8 w-8 p-0 border-neutral-200 hover:border-red-600 hover:bg-red-600 hover:text-white transition-all cursor-pointer"
>
<Square className="w-4 h-4" />
</Button>
}
/>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<Button
size="sm"
variant="outline"
onClick={() => {
if (!isWorkspaceActive) {
// TODO: 启动工作区
} else {
getWorkspaceDetail(workspace)
}
}}
className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer"
>
{isWorkspaceActive ? <Eye className="w-4 h-4" /> : <Play className="w-4 h-4" />}
</Button>
}
/>
<TooltipContent>
<p>{isWorkspaceActive ? '查看工作区' : '启动工作区'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<a
href={repo.web_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-neutral-600 hover:text-neutral-900 transition-colors shrink-0"
>
<ExternalLink className="w-4 h-4" />
CNB
</a>
</div>
<a
href={repo.web_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-neutral-600 hover:text-neutral-900 transition-colors shrink-0"
>
<ExternalLink className="w-4 h-4" />
CNB
</a>
</div>
{/* 描述 */}
{repo.description && (
<p className="text-sm text-neutral-600 max-h-[4.5em] overflow-hidden truncate">
<p className="text-sm text-neutral-600 h-12 overflow-hidden truncate">
{repo.description}
</p>
)}
{/* 主题标签 */}
{repo.topics && (
<div className="flex flex-wrap gap-2">
{repo.topics.split(',').map((topic: string, idx: number) => (
<Badge key={idx} variant="outline" className="text-xs border-neutral-300 text-neutral-700">
{topic.trim()}
</Badge>
))}
</div>
)}
{/* 主题标签和知识库 */}
<div className="flex items-center gap-2">
{/* 主题标签 */}
{repo.topics && (
<div className="flex flex-wrap gap-2">
{repo.topics.split(',').map((topic: string, idx: number) => (
<Badge key={idx} variant="outline" className="text-xs border-neutral-300 text-neutral-700">
{topic.trim()}
</Badge>
))}
</div>
)}
{/* 语言和更新时间 */}
</div>
{/* 统计信息 */}
<div className="flex items-center gap-6 text-xs text-neutral-500">
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
<Star className="w-3.5 h-3.5" />
<span className="font-medium">{repo.star_count}</span>
</span>
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
<GitFork className="w-3.5 h-3.5" />
<span className="font-medium">{repo.fork_count}</span>
</span>
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
<FileText className="w-3.5 h-3.5" />
<span className="font-medium">{repo.open_issue_count}</span>
</span>
{isWorkspaceActive && (
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors cursor-pointer">
<Play className="w-3.5 h-3.5" />
<span className="font-medium"></span>
</span>
)}
{isMine && (
<span
className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors cursor-pointer"
onClick={() => {
setSelectedSyncRepo(repo)
setSyncDialogOpen(true)
}}
>
<RefreshCw className="w-3.5 h-3.5" />
<span className="font-medium"></span>
</span>
)}
</div>
{/* 更新信息 */}
<div className="flex items-center gap-6 text-xs text-neutral-500">
{repo.last_update_nickname && (
<span className="flex items-center gap-1">

View File

@@ -59,7 +59,7 @@ export function EditRepoDialog({ open, onOpenChange, repo }: EditRepoDialogProps
const onSubmit = async (data: FormData) => {
if (!repo) return
await updateRepoInfo({
path: repo.path,
description: data.description?.trim() || '',
@@ -67,8 +67,8 @@ export function EditRepoDialog({ open, onOpenChange, repo }: EditRepoDialogProps
topics: tags.join(','),
license: data.license?.trim() || '',
})
await getList(true)
await getList({}, true)
onOpenChange(false)
}

View File

@@ -165,6 +165,21 @@ export const App = () => {
</div>
</footer>
<CommonRepoDialog />
</div>
)
}
export const CommonRepoDialog = () => {
const { editRepo, showEditDialog, setShowEditDialog, showCreateDialog, setShowCreateDialog, } = useRepoStore(useShallow((state) => ({
editRepo: state.editRepo,
showEditDialog: state.showEditDialog,
setShowEditDialog: state.setShowEditDialog,
showCreateDialog: state.showCreateDialog,
setShowCreateDialog: state.setShowCreateDialog,
})))
return (
<>
<EditRepoDialog
open={showEditDialog}
onOpenChange={setShowEditDialog}
@@ -176,8 +191,7 @@ export const App = () => {
/>
<WorkspaceDetailDialog />
<SyncRepoDialog />
</div>
</>
)
}
export default App;

View File

@@ -1,18 +1,27 @@
import { useSearch } from "@tanstack/react-router";
import { useRepoStore } from "../store";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useShallow } from "zustand/shallow";
import { RepoInfoCard } from "../components/RepoInfoCard";
import BuildConfig from "../components/BuildConfig";
import { CommonRepoDialog } from "../page";
export const App = () => {
const params = useSearch({ strict: false }) as { repo?: string };
const repoStore = useRepoStore(useShallow((state) => ({
getItem: state.getItem,
editRepo: state.editRepo,
refresH: state.refresh,
})));
const [activeTab, setActiveTab] = useState("build");
const tabs = [
{ key: "build", label: "构建配置" },
{ key: "info", label: "基本信息" },
]
useEffect(() => {
if (params.repo) {
repoStore.getItem(params.repo);
repoStore.refresH({ search: params.repo, showTips: false });
} else {
console.log('no repo param')
}
@@ -21,10 +30,30 @@ export const App = () => {
return <div>Loading...</div>
}
return (
<div className="p-2">
<div className="p-2 flex-col flex gap-2 h-full">
<div className="px-4">
<RepoInfoCard />
</div>
<div className="px-4 h-[calc(100%-200px)] scrollbar flex-col flex gap-4 overflow-hidden">
<div className="flex border-b mb-4">
{tabs.map(tab => (
<div
key={tab.key}
className={`px-4 py-2 cursor-pointer ${activeTab === tab.key ? 'border-b-2 border-gray-500' : ''}`}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</div>
))}
</div>
{activeTab === 'build' && <BuildConfig />}
{activeTab === 'info' && (
<div className="p-4 border rounded bg-white h-full overflow-auto scrollbar">
<pre className="whitespace-pre-wrap break-all">{JSON.stringify(repoStore.editRepo, null, 2)}</pre>
</div>
)}
</div>
<CommonRepoDialog />
</div>
)
}

View File

@@ -49,4 +49,58 @@ export const createCommitBlankConfig = (params: { repo?: string, event: 'api_tri
git commit --allow-empty -m "up: ${now}"
git push
`
}
export const createDevConfig = (params: { repo?: string, event?: string }) => {
const event = params?.event || 'api_trigger_event';
return `##### 配置开始,保留注释 #####
.common_env: &common_env
env:
# 使用环境变量管理密钥,推荐使用密钥仓库管理密钥, 详情见 readme.md
# 使用仓库密钥时,注释
## 可选 API-Key 配置(按需取消注释)
# MINIMAX_API_KEY: '' # Minimax 模型
# ZHIPU_API_KEY: '' # 智谱 AI
# BAILIAN_CODE_API_KEY: '' # 阿里云百炼
# VOLCENGINE_API_KEY: '' # 火山引擎
# CNB_API_KEY: '' # CNB API
# 可选应用配置
# FEISHU_APP_ID: '' # 飞书应用 ID
# FEISHU_APP_SECRET: '' # 飞书应用密钥
# CNB_COOKIE: '' # 可选配置用cnb.cool的cookie
USERNAME: root
ASSISTANT_CONFIG_DIR: /workspace/kevisual # ASSISTANT_CONFIG_DIR 环境变量指定了配置文件所在的目录
# CNB_KEVISUAL_ORG: kevisual # 私密仓库使用环境配置默认即可默认为当前用户组CNB_GROUP_SLUG
# CNB_KEVISUAL_APP: assistant-app # 可选配置(默认即可)
# CNB_OPENCLAW: openclaw # 仓库名(默认即可)
# CNB_OPENWEBUI: open-webui # 仓库名(默认即可)
##### 配置结束 #####
main:
${event}:
- docker:
image: docker.cnb.cool/kevisual/dev-env:latest
services:
- vscode
- docker
runner:
cpus: 16
imports:
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
env: !reference [.common_env, env]
stages:
- name: 环境变量
script: printenv > ~/.env.development
- name: 启动nginx
script: nginx
- name: 初始化开发机
script: zsh /workspace/scripts/init.sh
# endStages:
# - name: 结束阶段
# script: bun /workspace/scripts/end.ts
`
}

View File

@@ -3,7 +3,8 @@ import { query } from '@/modules/query';
import { toast } from 'sonner';
import { cnb } from '@/agents/app'
import { WorkspaceInfo } from '@kevisual/cnb'
import { createBuildConfig, createCommitBlankConfig } from './build';
import { createBuildConfig, createCommitBlankConfig, createDevConfig } from './build';
import { useLayoutStore } from '@/pages/auth/store';
interface DisplayModule {
activity: boolean;
contributors: boolean;
@@ -52,6 +53,12 @@ interface Data {
type WorkspaceTabType = 'dev' | 'work'
type BuildConfig = {
repo: string;
branch: string;
event: string;
config: string;
}
type State = {
formData: Record<string, any>;
setFormData: (data: Record<string, any>) => void;
@@ -68,13 +75,13 @@ type State = {
setShowEditDialog: (show: boolean) => void;
showCreateDialog: boolean;
setShowCreateDialog: (show: boolean) => void;
getList: (silent?: boolean) => Promise<any>;
getList: (params?: { search?: string }, silent?: boolean) => 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>;
workspaceList: WorkspaceInfo[];
getWorkspaceList: () => Promise<any>;
refresh: (opts?: { message?: string, showTips?: boolean }) => Promise<any>;
refresh: (opts?: { message?: string, showTips?: boolean, search?: string }) => Promise<any>;
startWorkspace: (data: Partial<Data>, params?: { open?: boolean, branch?: string }) => Promise<any>;
stopWorkspace: (workspace?: WorkspaceInfo) => Promise<any>;
getWorkspaceDetail: (data: WorkspaceInfo) => Promise<any>;
@@ -89,6 +96,11 @@ type State = {
buildSync: (data: Partial<Data>, params: { toRepo?: string, fromRepo?: string }) => Promise<any>;
buildUpdate: (data: Partial<Data>, params?: any) => Promise<any>;
getItem: (repo: string) => Promise<any>;
buildConfig: BuildConfig | null;
setBuildConfig: (config: BuildConfig | null, save?: boolean) => Promise<any>;
deleteBuildConfig: (params: { repo: Data, user?: any }) => Promise<any>;
initBuildConfig: (params: { repo: Data, user?: any }) => Promise<any>;
buildWorkspace: () => Promise<any>;
}
export const useRepoStore = create<State>((set, get) => {
@@ -114,6 +126,127 @@ export const useRepoStore = create<State>((set, get) => {
setSyncDialogOpen: (open) => set({ syncDialogOpen: open }),
selectedSyncRepo: null,
setSelectedSyncRepo: (repo) => set({ selectedSyncRepo: repo }),
buildConfig: null,
setBuildConfig: async (config, save = true) => {
const me = useLayoutStore.getState().me;
if (config && config!.repo && save) {
let path: string = config.repo || '';
path = path.replace(/\//g, '__');
const key = `buildConfig_${path}.json`;
if (config && me) {
const res = await query.post({
path: 'config',
key: 'update',
data: {
key: key,
data: config,
}
})
if (res.code === 200) {
toast.success('配置已保存')
} else {
toast.error(res.message || '配置保存失败')
}
} else if (config) {
localStorage.setItem(key, JSON.stringify(config));
}
}
set({ buildConfig: config })
},
deleteBuildConfig: async (params: { repo: Data, user?: any }) => {
const repo = params.repo;
let path: string = repo.path || '';
path = path.replace(/\//g, '__');
const key = `buildConfig_${path}.json`;
if (params?.user) {
const res = await query.post({
path: 'config',
key: 'delete',
data: {
key: key,
}
})
if (res.code === 200) {
toast.success('配置已删除')
} else {
toast.error(res.message || '配置删除失败')
}
} else {
localStorage.removeItem(key);
toast.success('配置已删除')
}
},
initBuildConfig: async (params) => {
const repo = params.repo;
if (!repo) {
toast.error('仓库数据异常');
return;
}
set({ loading: true })
try {
console.log('初始化构建配置', params)
let path: string = repo.path || '';
path = path.replace(/\//g, '__');
const key = `buildConfig_${path}.json`;
if (params?.user) {
const res = await query.post({
path: 'config',
key: 'get',
data: {
key: key,
}
})
if (res.code === 200 && res.data?.data) {
set({ buildConfig: res.data.data })
return;
}
} else {
const localConfig = localStorage.getItem(key);
if (localConfig) {
try {
const config = JSON.parse(localConfig);
set({ buildConfig: config })
return;
} catch (e) {
console.error('本地配置解析失败', e);
}
}
}
const config: BuildConfig = {
repo: repo.path,
branch: 'main',
event: 'api_trigger_event',
config: createDevConfig({ repo: repo.path, event: 'api_trigger_event' }),
}
set({ buildConfig: config })
} catch (e) {
toast.error('配置加载失败');
console.error('配置加载失败', e);
}
finally {
set({ loading: false })
}
},
buildWorkspace: async () => {
const config = get().buildConfig;
if (!config) {
toast.error('请先保存构建配置');
return;
}
const res = await cnb.build.startBuild(config.repo, {
branch: config.branch,
env: {},
event: config.event,
config: config.config,
})
if (res.code === 200) {
toast.success('构建已触发')
} else {
toast.error(res.message || '构建触发失败')
}
},
getItem: async (repo: string) => {
const { setLoading } = get();
setLoading(true);
@@ -129,13 +262,19 @@ export const useRepoStore = create<State>((set, get) => {
setLoading(false);
}
},
getList: async (silent = false) => {
getList: async (params?: { search?: string }, silent = false) => {
const { setLoading } = get();
if (!silent) {
setLoading(true);
}
try {
const res = await cnb.repo.getRepoList({})
let opts = {}
if (params?.search) {
opts = {
search: params.search
}
}
const res = await cnb.repo.getRepoList(opts)
if (res.code === 200) {
const list = res.data! || []
set({ list });
@@ -171,8 +310,8 @@ export const useRepoStore = create<State>((set, get) => {
toast.error(res.message || '更新失败');
}
},
refresh: async (opts?: { message?: string, showTips?: boolean }) => {
const getList = get().getList();
refresh: async (opts?: { message?: string, showTips?: boolean, search?: string }) => {
const getList = get().getList({ search: opts?.search }, true);
const getWorkspaceList = get().getWorkspaceList();
await Promise.all([getList, getWorkspaceList]);
if (opts?.showTips !== false) {
@@ -207,7 +346,7 @@ export const useRepoStore = create<State>((set, get) => {
if (res.code === 200) {
toast.success('删除成功');
// 刷新列表
await get().getList(true);
await get().getList({}, true);
} else {
toast.error(res.message || '删除失败');
}
@@ -216,7 +355,7 @@ export const useRepoStore = create<State>((set, get) => {
if (e.message?.includes('JSON') || e.message?.includes('json')) {
toast.success('删除成功');
// 刷新列表
await get().getList(true);
await get().getList({}, true);
} else {
toast.error('删除失败');
console.error('删除错误:', e);