generated from kevisual/vite-react-template
feat: add CNB Board live information page with domain management
- Created a new page for managing domains with a table view and modal for editing. - Implemented Zustand store for domain state management including fetching, updating, and deleting domains. - Added components for displaying information cards with search functionality. - Integrated API calls for fetching CNB Board live data including build, repo, pull, NPC, and comment information. - Established routing for the CNB Board page.
This commit is contained in:
148
src/pages/cnb-board/components/InfoCard.tsx
Normal file
148
src/pages/cnb-board/components/InfoCard.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import Fuse from 'fuse.js';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Kbd } from '@/components/ui/kbd';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
interface InfoItem {
|
||||
title: string;
|
||||
value: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface InfoCardProps {
|
||||
title: string;
|
||||
list: InfoItem[];
|
||||
}
|
||||
|
||||
const MAX_VALUE_LENGTH = 200;
|
||||
|
||||
function isUrl(value: string): boolean {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function InfoCard({ title, list }: InfoCardProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const fuse = useMemo(
|
||||
() =>
|
||||
new Fuse(list, {
|
||||
keys: ['title', 'value', 'description'],
|
||||
threshold: 0.3,
|
||||
}),
|
||||
[list]
|
||||
);
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
if (!search.trim()) return list;
|
||||
return fuse.search(search).map((result) => result.item);
|
||||
}, [search, fuse, list]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>
|
||||
{search ? `匹配 ${filteredList.length} / ${list.length} 个变量` : `共 ${list.length} 个变量`}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="搜索..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-48 h-8"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-2">
|
||||
{filteredList.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
未找到匹配项
|
||||
</div>
|
||||
) : (
|
||||
filteredList.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="grid grid-cols-[240px_1fr] gap-4 py-2 border-b last:border-0"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Badge variant="outline" className="w-fit font-mono text-xs">
|
||||
{item.title}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
{item.value ? (
|
||||
isUrl(item.value) ? (
|
||||
<a
|
||||
href={item.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-fit font-mono text-xs text-blue-500 hover:underline break-all"
|
||||
>
|
||||
{item.value}
|
||||
</a>
|
||||
) : item.value.length > MAX_VALUE_LENGTH ? (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Kbd className="w-full h-full flex flex-col items-start font-mono text-xs break-all cursor-pointer hover:text-primary">
|
||||
<pre className='text-left'>
|
||||
{item.value.slice(0, MAX_VALUE_LENGTH)}...
|
||||
</pre>
|
||||
<div className="text-primary ml-1">(点击展开)</div>
|
||||
</Kbd>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-200! max-w-[90vw]! overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{item.title}</DialogTitle>
|
||||
<DialogDescription>{item.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="text-sm whitespace-pre-wrap break-all font-mono bg-muted p-2 rounded scrollbar">
|
||||
{item.value}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<Kbd className="w-fit font-mono text-xs break-all">
|
||||
{item.value}
|
||||
</Kbd>
|
||||
)
|
||||
) : (
|
||||
<Kbd className="w-fit font-mono text-xs text-muted-foreground">
|
||||
(空)
|
||||
</Kbd>
|
||||
)}
|
||||
{item.description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user