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();
|
const token = await queryLogin.getToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
set({ me: {} })
|
set({ me: {} })
|
||||||
const me = await queryLogin.getMe();
|
let me: UserInfo | undefined = undefined;
|
||||||
// const user = await queryLogin.checkLocalUser() as UserInfo;
|
|
||||||
const user = me.code === 200 ? me.data : undefined;
|
const _user = await queryLogin.checkLocalUser() as UserInfo;
|
||||||
if (user) {
|
if (_user) {
|
||||||
set({ me: user });
|
me = _user;
|
||||||
set({ isAdmin: user.orgs?.includes?.('admin') || false });
|
}
|
||||||
|
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 {
|
} else {
|
||||||
set({ me: undefined, isAdmin: false });
|
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
|
getUrl: (data) => data.codebuddycn
|
||||||
},
|
},
|
||||||
].sort((a, b) => (a.order || 0) - (b.order || 0))
|
].sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||||
console.log('workspaceLink', selectWorkspace)
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={showWorkspaceDialog} onOpenChange={setShowWorkspaceDialog}>
|
<Dialog open={showWorkspaceDialog} onOpenChange={setShowWorkspaceDialog}>
|
||||||
<DialogContent className="max-w-md! bg-white">
|
<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;
|
||||||
@@ -21,7 +21,7 @@ interface Data {
|
|||||||
freeze: boolean;
|
freeze: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
// Public, Private
|
// Public, Private
|
||||||
visibility_level: string;
|
visibility_level: string;
|
||||||
flags: string;
|
flags: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@@ -88,6 +88,7 @@ type State = {
|
|||||||
setSelectedSyncRepo: (repo: Data | null) => void;
|
setSelectedSyncRepo: (repo: Data | null) => void;
|
||||||
buildSync: (data: Partial<Data>, params: { toRepo?: string, fromRepo?: string }) => Promise<any>;
|
buildSync: (data: Partial<Data>, params: { toRepo?: string, fromRepo?: string }) => Promise<any>;
|
||||||
buildUpdate: (data: Partial<Data>, params?: any) => Promise<any>;
|
buildUpdate: (data: Partial<Data>, params?: any) => Promise<any>;
|
||||||
|
getItem: (repo: string) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRepoStore = create<State>((set, get) => {
|
export const useRepoStore = create<State>((set, get) => {
|
||||||
@@ -113,17 +114,14 @@ export const useRepoStore = create<State>((set, get) => {
|
|||||||
setSyncDialogOpen: (open) => set({ syncDialogOpen: open }),
|
setSyncDialogOpen: (open) => set({ syncDialogOpen: open }),
|
||||||
selectedSyncRepo: null,
|
selectedSyncRepo: null,
|
||||||
setSelectedSyncRepo: (repo) => set({ selectedSyncRepo: repo }),
|
setSelectedSyncRepo: (repo) => set({ selectedSyncRepo: repo }),
|
||||||
getItem: async (id) => {
|
getItem: async (repo: string) => {
|
||||||
const { setLoading } = get();
|
const { setLoading } = get();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await query.post({
|
const res = await cnb.repo.getRepo(repo)
|
||||||
path: 'demo',
|
|
||||||
key: 'item',
|
|
||||||
data: { id }
|
|
||||||
})
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
return res;
|
const data = res.data!;
|
||||||
|
set({ editRepo: data })
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.message || '请求失败');
|
toast.error(res.message || '请求失败');
|
||||||
}
|
}
|
||||||
@@ -376,6 +374,7 @@ export const useRepoStore = create<State>((set, get) => {
|
|||||||
toast.error(res.message || '更新失败')
|
toast.error(res.message || '更新失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
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 ConfigIndexRouteImport } from './routes/config/index'
|
||||||
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
|
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
|
||||||
|
|
||||||
@@ -24,6 +25,11 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const RepoIndexRoute = RepoIndexRouteImport.update({
|
||||||
|
id: '/repo/',
|
||||||
|
path: '/repo/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ConfigIndexRoute = ConfigIndexRouteImport.update({
|
const ConfigIndexRoute = ConfigIndexRouteImport.update({
|
||||||
id: '/config/',
|
id: '/config/',
|
||||||
path: '/config/',
|
path: '/config/',
|
||||||
@@ -40,12 +46,14 @@ export interface FileRoutesByFullPath {
|
|||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/config/gitea': typeof ConfigGiteaRoute
|
'/config/gitea': typeof ConfigGiteaRoute
|
||||||
'/config/': typeof ConfigIndexRoute
|
'/config/': typeof ConfigIndexRoute
|
||||||
|
'/repo/': typeof RepoIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/config/gitea': typeof ConfigGiteaRoute
|
'/config/gitea': typeof ConfigGiteaRoute
|
||||||
'/config': typeof ConfigIndexRoute
|
'/config': typeof ConfigIndexRoute
|
||||||
|
'/repo': typeof RepoIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -53,13 +61,14 @@ export interface FileRoutesById {
|
|||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/config/gitea': typeof ConfigGiteaRoute
|
'/config/gitea': typeof ConfigGiteaRoute
|
||||||
'/config/': typeof ConfigIndexRoute
|
'/config/': typeof ConfigIndexRoute
|
||||||
|
'/repo/': typeof RepoIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/login' | '/config/gitea' | '/config/'
|
fullPaths: '/' | '/login' | '/config/gitea' | '/config/' | '/repo/'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/login' | '/config/gitea' | '/config'
|
to: '/' | '/login' | '/config/gitea' | '/config' | '/repo'
|
||||||
id: '__root__' | '/' | '/login' | '/config/gitea' | '/config/'
|
id: '__root__' | '/' | '/login' | '/config/gitea' | '/config/' | '/repo/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -67,6 +76,7 @@ export interface RootRouteChildren {
|
|||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
ConfigGiteaRoute: typeof ConfigGiteaRoute
|
ConfigGiteaRoute: typeof ConfigGiteaRoute
|
||||||
ConfigIndexRoute: typeof ConfigIndexRoute
|
ConfigIndexRoute: typeof ConfigIndexRoute
|
||||||
|
RepoIndexRoute: typeof RepoIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -85,6 +95,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/repo/': {
|
||||||
|
id: '/repo/'
|
||||||
|
path: '/repo'
|
||||||
|
fullPath: '/repo/'
|
||||||
|
preLoaderRoute: typeof RepoIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/config/': {
|
'/config/': {
|
||||||
id: '/config/'
|
id: '/config/'
|
||||||
path: '/config'
|
path: '/config'
|
||||||
@@ -107,6 +124,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
ConfigGiteaRoute: ConfigGiteaRoute,
|
ConfigGiteaRoute: ConfigGiteaRoute,
|
||||||
ConfigIndexRoute: ConfigIndexRoute,
|
ConfigIndexRoute: ConfigIndexRoute,
|
||||||
|
RepoIndexRoute: RepoIndexRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._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