feat: 添加制品中心页面及相关组件,支持创建和编辑制品功能;更新README文档

This commit is contained in:
xiongxiao
2026-03-23 18:45:34 +08:00
committed by cnb
parent 482206129f
commit b0acbbf337
13 changed files with 717 additions and 28 deletions

View File

@@ -1,16 +1,57 @@
# cnb center # cnb center
一个应用工作台 一个应用工作台
cnb center 是对 cnb 的部分操作的可视化管理,是对 cnb 功能的补充,核心使用还是需要去使用 cnb。
## 功能 ## 功能
1. 对话管理 1. 对话管理
2. 公共资源 2. 公共资源
3. 仓库管理 3. 仓库管理
4.开发(cloud-env) 4.端环境(cloud-env)
5. 我的应用 5. 我的应用
- NPC - NPC
- Agent 管理 - Agent 管理
- 任务管理
6. 其他 6. 其他
- 制品中心 - 制品中心
- 配置 - 历史记录
- 配置
## 对话管理功能
列出对应的知识库,知识库是可以动态添加的, 点击后对话应用会加载对应的知识库进行对话,对话过程可以执行实际的任务, 例如调用接口,执行命令等。
配置routes转为指令
## 公共资源
分享的快速启动的程序应用宝库技能对话npcagent 等等。
## 仓库管理
基本的仓库管理,添加和编辑仓库,启动云段环境。
## 云端环境(cloud-env)
单独的云端环境管理,提供云端环境的停止,查看。
## 我的应用
任务管理
## 其他
### 制品中心
添加对应的仓库,查看对应的制品。对 docker 镜像支持快速配置同步到制品中心。
### 配置
因为调用 cnb 需要一些配置,例如 token和 cookie等提供一个界面来配置这些内容方便使用。
### 历史记录
对话历史记录,任务执行历史记录等。

3
plan.md Normal file
View File

@@ -0,0 +1,3 @@
# cnb-center
是对 cnb 的部分操作的可视化管理,是对 cnb 功能的补充,核心使用还是需要去使用 cnb。

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useCloudEnvStore } from './store' import { useCloudEnvStore } from './store'
import { useRepoStore } from '../repos/store'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -25,11 +26,14 @@ import {
Wind, Wind,
Plane, Plane,
Rocket, Rocket,
ExternalLink ExternalLink,
Info
} from 'lucide-react' } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { WorkspaceInfo } from '@kevisual/cnb' import { WorkspaceInfo } from '@kevisual/cnb'
import clsx from 'clsx' import clsx from 'clsx'
import { WorkspaceDetailDialog } from '../repos/modules/WorkspaceDetailDialog'
import { useShallow } from 'zustand/shallow'
type WorkspaceOpen = { type WorkspaceOpen = {
url?: string url?: string
@@ -61,13 +65,7 @@ const linkItems: LinkItem[] = [
{ key: 'jumpUrl', label: 'Jump', icon: <ExternalLink className="w-5 h-5" />, getUrl: (d) => d.jumpUrl }, { key: 'jumpUrl', label: 'Jump', icon: <ExternalLink className="w-5 h-5" />, getUrl: (d) => d.jumpUrl },
{ key: 'webide', label: 'Web IDE', icon: <Code2 className="w-5 h-5" />, getUrl: (d) => d.webide }, { key: 'webide', label: 'Web IDE', icon: <Code2 className="w-5 h-5" />, getUrl: (d) => d.webide },
{ key: 'vscode', label: 'VS Code', icon: <Code2 className="w-5 h-5" />, getUrl: (d) => d.vscode }, { key: 'vscode', label: 'VS Code', icon: <Code2 className="w-5 h-5" />, getUrl: (d) => d.vscode },
{ key: 'cursor', label: 'Cursor', icon: <MousePointer2 className="w-5 h-5" />, getUrl: (d) => d.cursor },
{ key: 'trae-cn', label: 'Trae', icon: <Rocket className="w-5 h-5" />, getUrl: (d) => d['trae-cn'] },
{ key: 'windsurf', label: 'Windsurf', icon: <Wind className="w-5 h-5" />, getUrl: (d) => d.windsurf },
{ key: 'antigravity', label: 'Antigravity', icon: <Plane className="w-5 h-5" />, getUrl: (d) => d.antigravity },
{ key: 'ssh', label: 'SSH', icon: <Lock className="w-5 h-5" />, getUrl: (d) => d.ssh }, { key: 'ssh', label: 'SSH', icon: <Lock className="w-5 h-5" />, getUrl: (d) => d.ssh },
{ key: 'remoteSsh', label: 'Remote SSH', icon: <Radio className="w-5 h-5" />, getUrl: (d) => d.remoteSsh },
{ key: 'codebuddycn', label: 'CodeBuddy', icon: <Zap className="w-5 h-5" />, getUrl: (d) => d.codebuddycn },
] ]
function LinkCard({ item, workspaceData }: { item: LinkItem; workspaceData: WorkspaceOpen }) { function LinkCard({ item, workspaceData }: { item: LinkItem; workspaceData: WorkspaceOpen }) {
@@ -127,6 +125,10 @@ function WorkspaceCard({ workspace, onStop }: { workspace: WorkspaceInfo; onStop
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [workspaceData, setWorkspaceData] = useState<WorkspaceOpen | null>(null) const [workspaceData, setWorkspaceData] = useState<WorkspaceOpen | null>(null)
const getWorkspaceDetail = useCloudEnvStore((state) => state.getWorkspaceDetail) const getWorkspaceDetail = useCloudEnvStore((state) => state.getWorkspaceDetail)
const repoStore = useRepoStore(useShallow((state) => ({
setShowWorkspaceDialog: state.setShowWorkspaceDialog,
setWorkspaceTab: state.setWorkspaceTab,
})))
useEffect(() => { useEffect(() => {
const fetchDetail = async () => { const fetchDetail = async () => {
@@ -138,6 +140,16 @@ function WorkspaceCard({ workspace, onStop }: { workspace: WorkspaceInfo; onStop
fetchDetail() fetchDetail()
}, [workspace, getWorkspaceDetail]) }, [workspace, getWorkspaceDetail])
const handleShowDetail = async () => {
const data = await getWorkspaceDetail(workspace)
useRepoStore.setState({
selectWorkspace: workspace,
workspaceLink: data || {},
showWorkspaceDialog: true,
workspaceTab: 'dev'
})
}
return ( return (
<Card className="p-4 space-y-4 border border-neutral-200 bg-white"> <Card className="p-4 space-y-4 border border-neutral-200 bg-white">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -153,15 +165,24 @@ function WorkspaceCard({ workspace, onStop }: { workspace: WorkspaceInfo; onStop
<Badge variant="outline" className="text-xs">{workspace.branch}</Badge> <Badge variant="outline" className="text-xs">{workspace.branch}</Badge>
)} )}
</div> </div>
<Button <div className="flex items-center gap-2">
size="sm" <Button
variant="outline" size="sm"
onClick={() => onStop(workspace)} variant="outline"
className="text-red-600 border-red-200 hover:bg-red-600 hover:text-white" onClick={handleShowDetail}
> >
<Square className="w-4 h-4 mr-1" /> <Info className="w-4 h-4" />
</Button>
</Button> <Button
size="sm"
variant="outline"
onClick={() => onStop(workspace)}
className="text-red-600 border-red-200 hover:bg-red-600 hover:text-white"
>
<Square className="w-4 h-4 mr-1" />
</Button>
</div>
</div> </div>
{loading ? ( {loading ? (
@@ -171,7 +192,7 @@ function WorkspaceCard({ workspace, onStop }: { workspace: WorkspaceInfo; onStop
))} ))}
</div> </div>
) : workspaceData ? ( ) : workspaceData ? (
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-2 gap-2">
{linkItems.map((item) => ( {linkItems.map((item) => (
<LinkCard key={item.key} item={item} workspaceData={workspaceData} /> <LinkCard key={item.key} item={item} workspaceData={workspaceData} />
))} ))}
@@ -234,6 +255,7 @@ export default function CloudEnvPage() {
</div> </div>
)} )}
</div> </div>
<WorkspaceDetailDialog />
</SidebarLayout> </SidebarLayout>
) )
} }

View File

@@ -0,0 +1,105 @@
import { useState } from "react";
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 { TagInput } from "./TagInput";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export function CreateDialog({ open, onClose, onSubmit }: {
open: boolean;
onClose: () => void;
onSubmit: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
}) {
const [title, setTitle] = useState('');
const [tags, setTags] = useState<string[]>([]);
const [link, setLink] = useState('');
const [summary, setSummary] = useState('');
const [description, setDescription] = useState('');
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async () => {
if (!title.trim()) {
alert('请输入标题');
return;
}
setSubmitting(true);
try {
await onSubmit({
title: title.trim(),
tags,
link: link.trim(),
summary: summary.trim(),
description: description.trim(),
});
} finally {
setSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-lg!">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="create-title"></Label>
<Input
id="create-title"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="请输入标题"
/>
</div>
<div className="space-y-2">
<Label></Label>
<TagInput value={tags} onChange={setTags} placeholder="输入标签后按回车添加" />
</div>
<div className="space-y-2">
<Label htmlFor="create-link"></Label>
<Input
id="create-link"
value={link}
onChange={e => setLink(e.target.value)}
placeholder="https://..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="create-summary"></Label>
<Input
id="create-summary"
value={summary}
onChange={e => setSummary(e.target.value)}
placeholder="简要描述"
/>
</div>
<div className="space-y-2">
<Label htmlFor="create-description"></Label>
<Textarea
id="create-description"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="详细描述..."
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={submitting}></Button>
<Button variant="outline" onClick={handleSubmit} disabled={submitting}>
{submitting ? '创建中...' : '创建'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,117 @@
import { useState, useEffect } from "react";
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 { TagInput } from "./TagInput";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export function EditDialog({ open, item, onClose, onSubmit }: {
open: boolean;
item: any;
onClose: () => void;
onSubmit: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
}) {
const [title, setTitle] = useState('');
const [tags, setTags] = useState<string[]>([]);
const [link, setLink] = useState('');
const [summary, setSummary] = useState('');
const [description, setDescription] = useState('');
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (open && item) {
setTitle(item.title || '');
setTags(item.tags || []);
setLink(item.link || '');
setSummary(item.summary || '');
setDescription(item.description || '');
}
}, [open, item]);
const handleSubmit = async () => {
if (!title.trim()) {
alert('请输入标题');
return;
}
if (!item?.id) return;
setSubmitting(true);
try {
await onSubmit(item.id, {
title: title.trim(),
tags,
link: link.trim(),
summary: summary.trim(),
description: description.trim(),
});
} finally {
setSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-lg!">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-title"></Label>
<Input
id="edit-title"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="请输入标题"
/>
</div>
<div className="space-y-2">
<Label></Label>
<TagInput value={tags} onChange={setTags} placeholder="输入标签后按回车添加" />
</div>
<div className="space-y-2">
<Label htmlFor="edit-link"></Label>
<Input
id="edit-link"
value={link}
onChange={e => setLink(e.target.value)}
placeholder="https://..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-summary"></Label>
<Input
id="edit-summary"
value={summary}
onChange={e => setSummary(e.target.value)}
placeholder="简要描述"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-description"></Label>
<Textarea
id="edit-description"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="详细描述..."
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={submitting}></Button>
<Button variant="outline" onClick={handleSubmit} disabled={submitting}>
{submitting ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,53 @@
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { XIcon } from "lucide-react";
export function TagInput({ value, onChange, placeholder }: {
value: string[];
onChange: (tags: string[]) => void;
placeholder?: string;
}) {
const [input, setInput] = useState('');
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && input.trim()) {
e.preventDefault();
if (!value.includes(input.trim())) {
onChange([...value, input.trim()]);
}
setInput('');
}
};
const handleRemove = (tag: string) => {
onChange(value.filter(t => t !== tag));
};
return (
<div className="space-y-2">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder || '输入标签后按回车添加'}
/>
{value.length > 0 && (
<div className="flex flex-wrap gap-1">
{value.map((tag) => (
<Badge key={tag} variant="outline" className="gap-1">
{tag}
<button
type="button"
onClick={() => handleRemove(tag)}
className="hover:text-destructive"
>
<XIcon className="size-3" />
</button>
</Badge>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,3 @@
export { CreateDialog } from "./CreateDialog";
export { EditDialog } from "./EditDialog";
export { TagInput } from "./TagInput";

View File

@@ -0,0 +1,153 @@
import { usePackageStore, type PackageState } from "./store";
import { useShallow } from "zustand/shallow";
import { useEffect, useState } from "react";
import dayjs from "dayjs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { CreateDialog, EditDialog } from "./components";
import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react";
import { SidebarLayout } from "@/pages/sidebar/components";
export const App = () => {
const packageStore = usePackageStore(useShallow((state: PackageState) => {
return {
list: state.list,
loading: state.loading,
getList: state.getList,
createItem: state.createItem,
updateItem: state.updateItem,
deleteItem: state.deleteItem,
showCreateDialog: state.showCreateDialog,
setShowCreateDialog: state.setShowCreateDialog,
showEditDialog: state.showEditDialog,
setShowEditDialog: state.setShowEditDialog,
editingItem: state.editingItem,
setEditingItem: state.setEditingItem,
}
}));
const [search, setSearch] = useState('');
useEffect(() => {
packageStore.getList({ search });
}, [search]);
const handleDelete = async (id: string) => {
if (confirm('确定要删除这个制品吗?')) {
await packageStore.deleteItem(id);
}
};
const handleRefresh = () => {
packageStore.getList({ search });
};
const handleEdit = (item: any) => {
packageStore.setEditingItem(item);
packageStore.setShowEditDialog(true);
};
const handleCreate = () => {
packageStore.setShowCreateDialog(true);
};
return (
<SidebarLayout>
<div className="p-5">
<div className="flex justify-between items-center mb-5">
<h1 className="text-2xl font-semibold"></h1>
<div className="flex gap-2">
<div className="relative">
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
type="text"
placeholder="搜索制品..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-8 w-48"
/>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh}>
<RefreshCwIcon className="size-4" />
</Button>
<Button variant="outline" size="sm" onClick={handleCreate}>
<PlusIcon className="size-4" />
</Button>
</div>
</div>
{packageStore.loading ? (
<div className="text-center py-10 text-muted-foreground">...</div>
) : packageStore.list.length === 0 ? (
<div className="text-center py-10 text-muted-foreground"></div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{packageStore.list.map((item) => (
<Card key={item.id}>
<CardHeader>
<CardTitle>{item.title || '未命名'}</CardTitle>
<CardDescription className="text-xs">ID: {item.id}</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{item.tags.map((tag, index) => (
<Badge key={index} variant="outline">{tag}</Badge>
))}
</div>
)}
{item.summary && (
<p className="text-sm text-muted-foreground">{item.summary}</p>
)}
{item.description && (
<p className="text-sm text-muted-foreground line-clamp-2">{item.description}</p>
)}
<p className="text-xs text-muted-foreground">
: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
</p>
<p className="text-xs text-muted-foreground">
: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
</p>
<div className="flex gap-2 pt-2">
<Button variant="outline" size="sm" onClick={() => handleEdit(item)}>
<PencilIcon className="size-3.5" />
</Button>
<Button variant="outline" size="sm" onClick={() => handleDelete(item.id)}>
<TrashIcon className="size-3.5" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
<CreateDialog
open={packageStore.showCreateDialog}
onClose={() => packageStore.setShowCreateDialog(false)}
onSubmit={packageStore.createItem}
/>
<EditDialog
open={packageStore.showEditDialog}
item={packageStore.editingItem}
onClose={() => {
packageStore.setShowEditDialog(false);
packageStore.setEditingItem(null);
}}
onSubmit={packageStore.updateItem}
/>
</div>
</SidebarLayout>
);
};
export default App;

View File

@@ -0,0 +1,154 @@
import { create } from 'zustand';
import { queryApi } from '@/modules/mark-api';
import { toast } from 'sonner';
type PackageItem = {
id: string;
title: string;
tags?: string[];
link?: string;
summary?: string;
description?: string;
createdAt: string;
updatedAt: string;
}
type PackageState = {
edit: boolean;
setEdit: (edit: boolean) => void;
list: PackageItem[];
loading: boolean;
setLoading: (loading: boolean) => void;
// Dialog states
showCreateDialog: boolean;
setShowCreateDialog: (show: boolean) => void;
showEditDialog: boolean;
setShowEditDialog: (show: boolean) => void;
editingItem: PackageItem | null;
setEditingItem: (item: PackageItem | null) => void;
// Data operations
getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise<void>;
createItem: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
updateItem: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
deleteItem: (id: string) => Promise<void>;
getItem: (id: string) => Promise<PackageItem | null>;
}
export type { PackageState, PackageItem };
export const usePackageStore = create<PackageState>((set, get) => ({
edit: false,
setEdit: (edit) => set({ edit }),
list: [],
loading: false,
setLoading: (loading) => set({ loading }),
showCreateDialog: false,
setShowCreateDialog: (show) => set({ showCreateDialog: show }),
showEditDialog: false,
setShowEditDialog: (show) => set({ showEditDialog: show }),
editingItem: null,
setEditingItem: (item) => set({ editingItem: item }),
getList: async (params = {}) => {
const { page = 1, pageSize = 20, search } = params;
set({ loading: true });
try {
const res = await queryApi.mark.list({
markType: 'cnb-packages',
page,
pageSize,
search,
sort: 'DESC'
});
if (res.code === 200) {
set({ list: res.data?.list || [] });
} else {
toast.error(res.message || '获取列表失败');
}
} catch (e) {
console.error('获取制品列表失败', e);
toast.error('获取列表失败');
} finally {
set({ loading: false });
}
},
createItem: async (data) => {
try {
const res = await queryApi.mark.create({
title: data.title,
markType: 'cnb-packages',
tags: data.tags || [],
link: data.link || '',
summary: data.summary || '',
description: data.description || ''
});
if (res.code === 200) {
toast.success('创建成功');
get().getList();
set({ showCreateDialog: false });
} else {
toast.error(res.message || '创建失败');
}
} catch (e) {
console.error('创建失败', e);
toast.error('创建失败');
}
},
updateItem: async (id, data) => {
try {
const res = await queryApi.mark.update({
data: {
// @ts-ignore
id,
title: data.title || '',
tags: data.tags || [],
link: data.link || '',
summary: data.summary || '',
description: data.description || ''
}
});
if (res.code === 200) {
toast.success('更新成功');
get().getList();
set({ showEditDialog: false, editingItem: null });
} else {
toast.error(res.message || '更新失败');
}
} catch (e) {
console.error('更新失败', e);
toast.error('更新失败');
}
},
deleteItem: async (id) => {
try {
const res = await queryApi.mark.delete({ id });
if (res.code === 200) {
toast.success('删除成功');
get().getList();
} else {
toast.error(res.message || '删除失败');
}
} catch (e) {
console.error('删除失败', e);
toast.error('删除失败');
}
},
getItem: async (id) => {
try {
const res = await queryApi.mark.get({ id });
if (res.code === 200) {
return res.data;
} else {
toast.error(res.message || '获取详情失败');
return null;
}
} catch (e) {
console.error('获取详情失败', e);
return null;
}
}
}));

View File

@@ -1,4 +1,4 @@
import { FolderKanban, LayoutDashboard, Settings, PlayCircle, Cloud } from 'lucide-react' import { FolderKanban, LayoutDashboard, Settings, PlayCircle, Cloud, Package } from 'lucide-react'
import { Sidebar, type NavItem } from '@/components/a/Sidebar' import { Sidebar, type NavItem } from '@/components/a/Sidebar'
import { Logo } from './CNBBlackLogo.tsx' import { Logo } from './CNBBlackLogo.tsx'
@@ -19,15 +19,22 @@ const navItems: NavItem[] = [
icon: <LayoutDashboard className="w-5 h-5" />, icon: <LayoutDashboard className="w-5 h-5" />,
}, },
{ {
title: '应用配置', title: '制品中心',
path: '/config', path: '/cnb-packages',
icon: <Settings className="w-5 h-5" />, icon: <Package className="w-5 h-5" />,
}, },
{ {
title: '其他', title: '其他',
path: '/demo', path: '/other',
icon: <PlayCircle className="w-5 h-5" />, icon: <PlayCircle className="w-5 h-5" />,
isDeveloping: true, children: [
{
title: '应用配置',
path: '/config',
icon: <Settings className="w-5 h-5" />,
},
]
}, },
] ]

View File

@@ -102,8 +102,9 @@ export const useWorkspaceStore = create<WorkspaceState>((set, get) => ({
updateItem: async (id, data) => { updateItem: async (id, data) => {
try { try {
const res = await queryApi.mark.update({ const res = await queryApi.mark.update({
id,
data: { data: {
// @ts-ignore
id,
title: data.title || '', title: data.title || '',
tags: data.tags || [], tags: data.tags || [],
link: data.link || '', link: data.link || '',

View File

@@ -15,6 +15,7 @@ import { Route as IndexRouteImport } from './routes/index'
import { Route as WorkspacesIndexRouteImport } from './routes/workspaces/index' import { Route as WorkspacesIndexRouteImport } from './routes/workspaces/index'
import { Route as RepoIndexRouteImport } from './routes/repo/index' import { Route as RepoIndexRouteImport } from './routes/repo/index'
import { Route as ConfigIndexRouteImport } from './routes/config/index' import { Route as ConfigIndexRouteImport } from './routes/config/index'
import { Route as CnbPackagesIndexRouteImport } from './routes/cnb-packages/index'
import { Route as CloudEnvIndexRouteImport } from './routes/cloud-env/index' import { Route as CloudEnvIndexRouteImport } from './routes/cloud-env/index'
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea' import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
@@ -48,6 +49,11 @@ const ConfigIndexRoute = ConfigIndexRouteImport.update({
path: '/config/', path: '/config/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const CnbPackagesIndexRoute = CnbPackagesIndexRouteImport.update({
id: '/cnb-packages/',
path: '/cnb-packages/',
getParentRoute: () => rootRouteImport,
} as any)
const CloudEnvIndexRoute = CloudEnvIndexRouteImport.update({ const CloudEnvIndexRoute = CloudEnvIndexRouteImport.update({
id: '/cloud-env/', id: '/cloud-env/',
path: '/cloud-env/', path: '/cloud-env/',
@@ -65,6 +71,7 @@ export interface FileRoutesByFullPath {
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/config/gitea': typeof ConfigGiteaRoute '/config/gitea': typeof ConfigGiteaRoute
'/cloud-env/': typeof CloudEnvIndexRoute '/cloud-env/': typeof CloudEnvIndexRoute
'/cnb-packages/': typeof CnbPackagesIndexRoute
'/config/': typeof ConfigIndexRoute '/config/': typeof ConfigIndexRoute
'/repo/': typeof RepoIndexRoute '/repo/': typeof RepoIndexRoute
'/workspaces/': typeof WorkspacesIndexRoute '/workspaces/': typeof WorkspacesIndexRoute
@@ -75,6 +82,7 @@ export interface FileRoutesByTo {
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/config/gitea': typeof ConfigGiteaRoute '/config/gitea': typeof ConfigGiteaRoute
'/cloud-env': typeof CloudEnvIndexRoute '/cloud-env': typeof CloudEnvIndexRoute
'/cnb-packages': typeof CnbPackagesIndexRoute
'/config': typeof ConfigIndexRoute '/config': typeof ConfigIndexRoute
'/repo': typeof RepoIndexRoute '/repo': typeof RepoIndexRoute
'/workspaces': typeof WorkspacesIndexRoute '/workspaces': typeof WorkspacesIndexRoute
@@ -86,6 +94,7 @@ export interface FileRoutesById {
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/config/gitea': typeof ConfigGiteaRoute '/config/gitea': typeof ConfigGiteaRoute
'/cloud-env/': typeof CloudEnvIndexRoute '/cloud-env/': typeof CloudEnvIndexRoute
'/cnb-packages/': typeof CnbPackagesIndexRoute
'/config/': typeof ConfigIndexRoute '/config/': typeof ConfigIndexRoute
'/repo/': typeof RepoIndexRoute '/repo/': typeof RepoIndexRoute
'/workspaces/': typeof WorkspacesIndexRoute '/workspaces/': typeof WorkspacesIndexRoute
@@ -98,6 +107,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/config/gitea' | '/config/gitea'
| '/cloud-env/' | '/cloud-env/'
| '/cnb-packages/'
| '/config/' | '/config/'
| '/repo/' | '/repo/'
| '/workspaces/' | '/workspaces/'
@@ -108,6 +118,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/config/gitea' | '/config/gitea'
| '/cloud-env' | '/cloud-env'
| '/cnb-packages'
| '/config' | '/config'
| '/repo' | '/repo'
| '/workspaces' | '/workspaces'
@@ -118,6 +129,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/config/gitea' | '/config/gitea'
| '/cloud-env/' | '/cloud-env/'
| '/cnb-packages/'
| '/config/' | '/config/'
| '/repo/' | '/repo/'
| '/workspaces/' | '/workspaces/'
@@ -129,6 +141,7 @@ export interface RootRouteChildren {
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
ConfigGiteaRoute: typeof ConfigGiteaRoute ConfigGiteaRoute: typeof ConfigGiteaRoute
CloudEnvIndexRoute: typeof CloudEnvIndexRoute CloudEnvIndexRoute: typeof CloudEnvIndexRoute
CnbPackagesIndexRoute: typeof CnbPackagesIndexRoute
ConfigIndexRoute: typeof ConfigIndexRoute ConfigIndexRoute: typeof ConfigIndexRoute
RepoIndexRoute: typeof RepoIndexRoute RepoIndexRoute: typeof RepoIndexRoute
WorkspacesIndexRoute: typeof WorkspacesIndexRoute WorkspacesIndexRoute: typeof WorkspacesIndexRoute
@@ -178,6 +191,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ConfigIndexRouteImport preLoaderRoute: typeof ConfigIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/cnb-packages/': {
id: '/cnb-packages/'
path: '/cnb-packages'
fullPath: '/cnb-packages/'
preLoaderRoute: typeof CnbPackagesIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/cloud-env/': { '/cloud-env/': {
id: '/cloud-env/' id: '/cloud-env/'
path: '/cloud-env' path: '/cloud-env'
@@ -201,6 +221,7 @@ const rootRouteChildren: RootRouteChildren = {
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
ConfigGiteaRoute: ConfigGiteaRoute, ConfigGiteaRoute: ConfigGiteaRoute,
CloudEnvIndexRoute: CloudEnvIndexRoute, CloudEnvIndexRoute: CloudEnvIndexRoute,
CnbPackagesIndexRoute: CnbPackagesIndexRoute,
ConfigIndexRoute: ConfigIndexRoute, ConfigIndexRoute: ConfigIndexRoute,
RepoIndexRoute: RepoIndexRoute, RepoIndexRoute: RepoIndexRoute,
WorkspacesIndexRoute: WorkspacesIndexRoute, WorkspacesIndexRoute: WorkspacesIndexRoute,

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import App from '@/pages/cnb-packages/page'
export const Route = createFileRoute('/cnb-packages/')({
component: RouteComponent,
})
function RouteComponent() {
return <App />
}