Skip to content

Commit 2eb0626

Browse files
committed
frontend: 数据管理联调
1 parent d8fbf49 commit 2eb0626

25 files changed

Lines changed: 1638 additions & 1293 deletions

frontend/src/components/CardView.tsx

Lines changed: 140 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import React from "react";
2-
import { Card, Tag, Pagination, Dropdown, Tooltip, Empty } from "antd";
1+
import React, { useState, useEffect, useRef } from "react";
2+
import { Card, Tag, Pagination, Dropdown, Tooltip, Empty, Popover } from "antd";
33
import {
44
EllipsisOutlined,
55
ClockCircleOutlined,
66
StarFilled,
77
} from "@ant-design/icons";
88
import type { ItemType } from "antd/es/menu/interface";
9+
import { formatDateTime } from "@/utils/unit";
910

1011
interface BaseCardDataType {
1112
id: string | number;
@@ -45,6 +46,113 @@ interface CardViewProps<T> {
4546
isFavorite?: (item: T) => boolean;
4647
}
4748

49+
// 标签渲染组件
50+
const TagsRenderer = ({ tags }: { tags?: any[] }) => {
51+
const [visibleTags, setVisibleTags] = useState<any[]>([]);
52+
const [hiddenTags, setHiddenTags] = useState<any[]>([]);
53+
const containerRef = useRef<HTMLDivElement>(null);
54+
55+
useEffect(() => {
56+
if (!tags || tags.length === 0) return;
57+
58+
const calculateVisibleTags = () => {
59+
if (!containerRef.current) return;
60+
61+
const containerWidth = containerRef.current.offsetWidth;
62+
const tempDiv = document.createElement("div");
63+
tempDiv.style.visibility = "hidden";
64+
tempDiv.style.position = "absolute";
65+
tempDiv.style.top = "-9999px";
66+
tempDiv.className = "flex flex-wrap gap-1";
67+
document.body.appendChild(tempDiv);
68+
69+
let totalWidth = 0;
70+
let visibleCount = 0;
71+
const tagElements: HTMLElement[] = [];
72+
73+
// 为每个tag创建临时元素来测量宽度
74+
tags.forEach((tag, index) => {
75+
const tagElement = document.createElement("span");
76+
tagElement.className = "ant-tag ant-tag-default";
77+
tagElement.style.margin = "2px";
78+
tagElement.textContent = typeof tag === "string" ? tag : tag.name;
79+
tempDiv.appendChild(tagElement);
80+
tagElements.push(tagElement);
81+
82+
const tagWidth = tagElement.offsetWidth + 4; // 加上gap的宽度
83+
84+
// 如果不是最后一个标签,需要预留+n标签的空间
85+
const plusTagWidth = index < tags.length - 1 ? 35 : 0; // +n标签大约35px宽度
86+
87+
if (totalWidth + tagWidth + plusTagWidth <= containerWidth) {
88+
totalWidth += tagWidth;
89+
visibleCount++;
90+
} else {
91+
// 如果当前标签放不下,且已经有可见标签,则停止
92+
if (visibleCount > 0) return;
93+
// 如果是第一个标签就放不下,至少显示一个
94+
if (index === 0) {
95+
totalWidth += tagWidth;
96+
visibleCount = 1;
97+
}
98+
}
99+
});
100+
101+
document.body.removeChild(tempDiv);
102+
103+
setVisibleTags(tags.slice(0, visibleCount));
104+
setHiddenTags(tags.slice(visibleCount));
105+
};
106+
107+
// 延迟执行以确保DOM已渲染
108+
const timer = setTimeout(calculateVisibleTags, 0);
109+
110+
// 监听窗口大小变化
111+
const handleResize = () => {
112+
calculateVisibleTags();
113+
};
114+
115+
window.addEventListener("resize", handleResize);
116+
117+
return () => {
118+
clearTimeout(timer);
119+
window.removeEventListener("resize", handleResize);
120+
};
121+
}, [tags]);
122+
123+
if (!tags || tags.length === 0) return null;
124+
125+
const popoverContent = (
126+
<div className="max-w-xs">
127+
<div className="flex flex-wrap gap-1">
128+
{hiddenTags.map((tag, index) => (
129+
<Tag key={index}>{typeof tag === "string" ? tag : tag.name}</Tag>
130+
))}
131+
</div>
132+
</div>
133+
);
134+
135+
return (
136+
<div ref={containerRef} className="flex flex-wrap gap-1 w-full">
137+
{visibleTags.map((tag, index) => (
138+
<Tag key={index}>{typeof tag === "string" ? tag : tag.name}</Tag>
139+
))}
140+
{hiddenTags.length > 0 && (
141+
<Popover
142+
content={popoverContent}
143+
title="更多标签"
144+
trigger="hover"
145+
placement="topLeft"
146+
>
147+
<Tag className="cursor-pointer bg-gray-100 border-gray-300 text-gray-600 hover:bg-gray-200">
148+
+{hiddenTags.length}
149+
</Tag>
150+
</Popover>
151+
)}
152+
</div>
153+
);
154+
};
155+
48156
function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
49157
const { data, pagination, operations, onView, onFavorite, isFavorite } =
50158
props;
@@ -64,13 +172,19 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
64172
<div className="flex-1 overflow-auto">
65173
<div className="grid md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
66174
{data.map((item) => (
67-
<Card key={item.id} className="hover:shadow-lg transition-shadow">
68-
<div className="space-y-4">
175+
<div
176+
key={item.id}
177+
className="border border-gray-100 rounded-lg p-4 bg-white hover:shadow-lg transition-shadow duration-200"
178+
>
179+
<div className="flex flex-col space-y-4 h-full">
69180
{/* Header */}
70181
<div className="flex items-start justify-between">
71182
<div className="flex items-center gap-3 min-w-0">
72183
<div
73-
className={`flex-shrink-0 w-10 h-10 ${item.iconColor} rounded-lg flex items-center justify-center`}
184+
className={`flex-shrink-0 w-10 h-10 ${
185+
item.iconColor ||
186+
"bg-gradient-to-br from-blue-100 to-blue-200"
187+
} rounded-lg flex items-center justify-center`}
74188
>
75189
{item.icon}
76190
</div>
@@ -105,34 +219,34 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
105219
)}
106220
</div>
107221

108-
{/* Tags */}
109-
<div className="flex flex-wrap gap-1">
110-
{item?.tags?.slice(0, 3)?.map((tag, index) => (
111-
<Tag key={index}>{tag}</Tag>
112-
))}
113-
</div>
114-
{/* Description */}
115-
<p className="text-gray-600 text-xs text-ellipsis overflow-hidden whitespace-nowrap text-xs line-clamp-2">
116-
<Tooltip title={item.description}>{item.description}</Tooltip>
117-
</p>
118-
{/* Statistics */}
119-
<div className="grid grid-cols-2 gap-4 py-3">
120-
{item.statistics?.map((stat, idx) => (
121-
<div key={idx}>
122-
<div className="text-sm text-gray-500">{stat.label}:</div>
123-
<div className="text-base font-semibold text-gray-900">
124-
{stat.value}
222+
<div className="flex-1 flex flex-col justify-end">
223+
{/* Tags */}
224+
<TagsRenderer tags={item?.tags} />
225+
226+
{/* Description */}
227+
<p className="text-gray-600 text-xs text-ellipsis overflow-hidden whitespace-nowrap text-xs line-clamp-2 mt-2">
228+
<Tooltip title={item.description}>{item.description}</Tooltip>
229+
</p>
230+
231+
{/* Statistics */}
232+
<div className="grid grid-cols-2 gap-4 py-3">
233+
{item.statistics?.map((stat, idx) => (
234+
<div key={idx}>
235+
<div className="text-sm text-gray-500">{stat.label}:</div>
236+
<div className="text-base font-semibold text-gray-900">
237+
{stat.value}
238+
</div>
125239
</div>
126-
</div>
127-
))}
240+
))}
241+
</div>
128242
</div>
129243

130244
{/* Actions */}
131245
<div className="flex items-center justify-between pt-3 border-t border-t-gray-200">
132246
<div className=" text-gray-500 text-right">
133247
<div className="flex items-center gap-1">
134248
<ClockCircleOutlined className="w-4 h-4" />{" "}
135-
{item.lastModified}
249+
{formatDateTime(item.updatedAt)}
136250
</div>
137251
</div>
138252
<Dropdown
@@ -153,7 +267,7 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
153267
</Dropdown>
154268
</div>
155269
</div>
156-
</Card>
270+
</div>
157271
))}
158272
</div>
159273
<div className="flex justify-end mt-6">

frontend/src/components/DetailHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,15 @@ const DetailHeader: React.FC<DetailHeaderProps<any>> = <T,>({
5353
className={`w-16 h-16 text-white rounded-xl flex items-center justify-center shadow-lg ${
5454
data?.iconColor
5555
? data.iconColor
56-
: "bg-gradient-to-br from-blue-500 to-blue-600"
56+
: "bg-gradient-to-br from-blue-100 to-blue-200"
5757
}`}
5858
>
5959
{data?.icon || <Database className="w-8 h-8" />}
6060
</div>
6161
<div className="flex-1">
6262
<div className="flex items-center gap-3 mb-2">
6363
<h1 className="text-lg font-bold text-gray-900">{data.name}</h1>
64-
{data.status && (
64+
{data?.status && (
6565
<Tag color={data.status.color}>
6666
<div className="flex items-center gap-2 text-xs">
6767
<span>{data.status.icon}</span>

0 commit comments

Comments
 (0)