generated from kevisual/vite-react-template
feat: 添加仓库信息卡片和仓库页面,优化仓库路由
This commit is contained in:
@@ -78,12 +78,19 @@ export const useLayoutStore = create<LayoutStore>((set, get) => ({
|
||||
const token = await queryLogin.getToken();
|
||||
if (token) {
|
||||
set({ me: {} })
|
||||
const me = await queryLogin.getMe();
|
||||
// const user = await queryLogin.checkLocalUser() as UserInfo;
|
||||
const user = me.code === 200 ? me.data : undefined;
|
||||
if (user) {
|
||||
set({ me: user });
|
||||
set({ isAdmin: user.orgs?.includes?.('admin') || false });
|
||||
let me: UserInfo | undefined = undefined;
|
||||
|
||||
const _user = await queryLogin.checkLocalUser() as UserInfo;
|
||||
if (_user) {
|
||||
me = _user;
|
||||
}
|
||||
if (!me) {
|
||||
const res = await queryLogin.getMe();
|
||||
me = res.code === 200 ? res.data : undefined;
|
||||
}
|
||||
if (me) {
|
||||
set({ me: me });
|
||||
set({ isAdmin: me.orgs?.includes?.('admin') || false });
|
||||
} else {
|
||||
set({ me: undefined, isAdmin: false });
|
||||
}
|
||||
|
||||
95
src/pages/repos/components/RepoInfoCard.tsx
Normal file
95
src/pages/repos/components/RepoInfoCard.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
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";
|
||||
|
||||
export const RepoInfoCard = () => {
|
||||
const repoStore = useRepoStore(useShallow((state) => ({
|
||||
getItem: state.getItem,
|
||||
editRepo: state.editRepo,
|
||||
})));
|
||||
const repo = repoStore.editRepo!;
|
||||
const onClone = () => {
|
||||
const url = `git clone https://cnb.cool/${repo.path}`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
toast.success('克隆地址已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
toast.error('复制失败')
|
||||
})
|
||||
}
|
||||
if (!repo) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部仓库信息卡片 */}
|
||||
<Card className="p-6 border border-neutral-200 bg-white">
|
||||
<div className="space-y-4">
|
||||
{/* 标题行 */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<span className="text-sm text-neutral-500 font-mono">
|
||||
{repo.path}
|
||||
</span>
|
||||
<button
|
||||
onClick={onClone}
|
||||
className="flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-900 transition-colors"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
克隆
|
||||
</button>
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{repo.visibility_level === 'Public' ? '公开' : repo.visibility_level === 'Private' ? '私有' : repo.visibility_level}
|
||||
</Badge>
|
||||
</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">
|
||||
{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-6 text-xs text-neutral-500">
|
||||
{repo.last_update_nickname && (
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="w-3.5 h-3.5" />
|
||||
{repo.last_update_nickname}
|
||||
</span>
|
||||
)}
|
||||
{repo.last_updated_at && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
{new Date(repo.last_updated_at).toLocaleDateString('zh-CN')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -237,7 +237,6 @@ export function WorkspaceDetailDialog() {
|
||||
getUrl: (data) => data.codebuddycn
|
||||
},
|
||||
].sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
console.log('workspaceLink', selectWorkspace)
|
||||
return (
|
||||
<Dialog open={showWorkspaceDialog} onOpenChange={setShowWorkspaceDialog}>
|
||||
<DialogContent className="max-w-md! bg-white">
|
||||
|
||||
32
src/pages/repos/repo/page.tsx
Normal file
32
src/pages/repos/repo/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { useRepoStore } from "../store";
|
||||
import { useEffect } from "react";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import { RepoInfoCard } from "../components/RepoInfoCard";
|
||||
|
||||
export const App = () => {
|
||||
const params = useSearch({ strict: false }) as { repo?: string };
|
||||
const repoStore = useRepoStore(useShallow((state) => ({
|
||||
getItem: state.getItem,
|
||||
editRepo: state.editRepo,
|
||||
})));
|
||||
useEffect(() => {
|
||||
if (params.repo) {
|
||||
repoStore.getItem(params.repo);
|
||||
} else {
|
||||
console.log('no repo param')
|
||||
}
|
||||
}, [params])
|
||||
if (!repoStore.editRepo) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className="px-4">
|
||||
<RepoInfoCard />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -88,6 +88,7 @@ type State = {
|
||||
setSelectedSyncRepo: (repo: Data | null) => void;
|
||||
buildSync: (data: Partial<Data>, params: { toRepo?: string, fromRepo?: string }) => Promise<any>;
|
||||
buildUpdate: (data: Partial<Data>, params?: any) => Promise<any>;
|
||||
getItem: (repo: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export const useRepoStore = create<State>((set, get) => {
|
||||
@@ -113,17 +114,14 @@ export const useRepoStore = create<State>((set, get) => {
|
||||
setSyncDialogOpen: (open) => set({ syncDialogOpen: open }),
|
||||
selectedSyncRepo: null,
|
||||
setSelectedSyncRepo: (repo) => set({ selectedSyncRepo: repo }),
|
||||
getItem: async (id) => {
|
||||
getItem: async (repo: string) => {
|
||||
const { setLoading } = get();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await query.post({
|
||||
path: 'demo',
|
||||
key: 'item',
|
||||
data: { id }
|
||||
})
|
||||
const res = await cnb.repo.getRepo(repo)
|
||||
if (res.code === 200) {
|
||||
return res;
|
||||
const data = res.data!;
|
||||
set({ editRepo: data })
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
@@ -376,6 +374,7 @@ export const useRepoStore = create<State>((set, get) => {
|
||||
toast.error(res.message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as RepoIndexRouteImport } from './routes/repo/index'
|
||||
import { Route as ConfigIndexRouteImport } from './routes/config/index'
|
||||
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
|
||||
|
||||
@@ -24,6 +25,11 @@ const IndexRoute = IndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const RepoIndexRoute = RepoIndexRouteImport.update({
|
||||
id: '/repo/',
|
||||
path: '/repo/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ConfigIndexRoute = ConfigIndexRouteImport.update({
|
||||
id: '/config/',
|
||||
path: '/config/',
|
||||
@@ -40,12 +46,14 @@ export interface FileRoutesByFullPath {
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/config/': typeof ConfigIndexRoute
|
||||
'/repo/': typeof RepoIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/config': typeof ConfigIndexRoute
|
||||
'/repo': typeof RepoIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@@ -53,13 +61,14 @@ export interface FileRoutesById {
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/config/': typeof ConfigIndexRoute
|
||||
'/repo/': typeof RepoIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/login' | '/config/gitea' | '/config/'
|
||||
fullPaths: '/' | '/login' | '/config/gitea' | '/config/' | '/repo/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/login' | '/config/gitea' | '/config'
|
||||
id: '__root__' | '/' | '/login' | '/config/gitea' | '/config/'
|
||||
to: '/' | '/login' | '/config/gitea' | '/config' | '/repo'
|
||||
id: '__root__' | '/' | '/login' | '/config/gitea' | '/config/' | '/repo/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@@ -67,6 +76,7 @@ export interface RootRouteChildren {
|
||||
LoginRoute: typeof LoginRoute
|
||||
ConfigGiteaRoute: typeof ConfigGiteaRoute
|
||||
ConfigIndexRoute: typeof ConfigIndexRoute
|
||||
RepoIndexRoute: typeof RepoIndexRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -85,6 +95,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/repo/': {
|
||||
id: '/repo/'
|
||||
path: '/repo'
|
||||
fullPath: '/repo/'
|
||||
preLoaderRoute: typeof RepoIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/config/': {
|
||||
id: '/config/'
|
||||
path: '/config'
|
||||
@@ -107,6 +124,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
LoginRoute: LoginRoute,
|
||||
ConfigGiteaRoute: ConfigGiteaRoute,
|
||||
ConfigIndexRoute: ConfigIndexRoute,
|
||||
RepoIndexRoute: RepoIndexRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
9
src/routes/repo/index.tsx
Normal file
9
src/routes/repo/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/repos/repo/page'
|
||||
export const Route = createFileRoute('/repo/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
Reference in New Issue
Block a user