Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/components/AddTagPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Tag {
interface AddTagPopoverProps {
tags: Tag[];
onFetchTags?: () => Promise<Tag[]>;
onAddTag?: (tag: Tag) => void;
onAddTag?: (tag: string) => void;
onCreateAndTag?: (tagName: string) => void;
}

Expand All @@ -39,7 +39,7 @@ export default function AddTagPopover({
};
useEffect(() => {
fetchTags();
}, [showPopover]);
}, [showPopover, onFetchTags]);

const availableTags = useMemo(() => {
return allTags.filter((tag) => !tagsSet.has(tag.id));
Expand Down
124 changes: 56 additions & 68 deletions frontend/src/components/CardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,78 +55,86 @@ interface CardViewProps<T> {

// 标签渲染组件
const TagsRenderer = ({ tags }: { tags?: Array<string | BadgeItem> }) => {
const [visibleTags, setVisibleTags] = useState<any[]>([]);
const [visibleTags, setVisibleTags] = useState<any[]>(tags || []);
const [hiddenTags, setHiddenTags] = useState<any[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const isInitializedRef = useRef(false);
const prevTagsRef = useRef<typeof tags>();

useEffect(() => {
if (!tags || tags.length === 0) return;
if (!tags || tags.length === 0) {
setVisibleTags([]);
setHiddenTags([]);
return;
}

const calculateVisibleTags = () => {
if (!containerRef.current) return;

const containerWidth = containerRef.current.offsetWidth;
const tempDiv = document.createElement("div");
tempDiv.style.visibility = "hidden";
tempDiv.style.position = "absolute";
tempDiv.style.top = "-9999px";
tempDiv.className = "flex flex-wrap gap-1";
document.body.appendChild(tempDiv);
const container = containerRef.current;
const containerRect = container.getBoundingClientRect();

let totalWidth = 0;
let visibleCount = 0;
const tagElements: HTMLElement[] = [];
// 获取所有标签元素
const tagElements = Array.from(container.querySelectorAll(':scope > .ant-tag'));
const plusTagElement = tagElements.find(el => el.textContent?.startsWith('+'));
const regularTags = tagElements.filter(el => !el.textContent?.startsWith('+'));

// 为每个tag创建临时元素来测量宽度
tags.forEach((tag, index) => {
const tagElement = document.createElement("span");
tagElement.className = "ant-tag ant-tag-default";
tagElement.style.margin = "2px";
if (typeof tag === "string") {
tagElement.textContent = tag;
} else {
tagElement.textContent = tag.label ? tag.label : tag.name;
}
tempDiv.appendChild(tagElement);
tagElements.push(tagElement);
// 动态测量 "+n" 标签宽度
let plusWidth = 0;
if (plusTagElement) {
plusWidth = plusTagElement.offsetWidth;
} else {
// 如果 "+n" 标签不存在,创建一个临时元素来测量
const tempPlus = document.createElement('span');
tempPlus.className = 'ant-tag ant-tag-default cursor-pointer bg-gray-100 border-gray-300 text-gray-600 hover:bg-gray-200';
tempPlus.textContent = '+99';
tempPlus.style.position = 'absolute';
tempPlus.style.visibility = 'hidden';
container.appendChild(tempPlus);
plusWidth = tempPlus.offsetWidth;
container.removeChild(tempPlus);
}

// 检查每个标签是否超出
let visibleCount = 0;
const containerRight = containerRect.right;
const gap = 4; // gap-1 的宽度

const tagWidth = tagElement.offsetWidth + 4; // 加上gap的宽度
regularTags.forEach((tagEl, index) => {
const tagRect = tagEl.getBoundingClientRect();
const tagRight = tagRect.right;
const remainingTags = tags.length - index - 1;
const needsPlusTag = remainingTags > 0;

// 如果不是最后一个标签,需要预留+n标签的空间
const plusTagWidth = index < tags.length - 1 ? 35 : 0; // +n标签大约35px宽度
// 精确计算:当前标签右边 + gap + (需要 "+n" 的话就加上 "+n" 宽度)
const totalWidthNeeded = tagRight + (needsPlusTag ? gap + plusWidth : 0);

if (totalWidth + tagWidth + plusTagWidth <= containerWidth) {
totalWidth += tagWidth;
if (totalWidthNeeded <= containerRight - 5) {
visibleCount++;
} else {
// 如果当前标签放不下,且已经有可见标签,则停止
if (visibleCount > 0) return;
// 如果是第一个标签就放不下,至少显示一个
if (index === 0) {
totalWidth += tagWidth;
visibleCount = 1;
}
return false;
}
});

document.body.removeChild(tempDiv);

setVisibleTags(tags.slice(0, visibleCount));
setHiddenTags(tags.slice(visibleCount));
};

// 延迟执行以确保DOM已渲染
const timer = setTimeout(calculateVisibleTags, 0);
// 多次尝试计算,确保 DOM 渲染完成
const timers = [
setTimeout(calculateVisibleTags, 0),
setTimeout(calculateVisibleTags, 50),
setTimeout(calculateVisibleTags, 100),
];

// 监听窗口大小变化
const handleResize = () => {
calculateVisibleTags();
};

window.addEventListener("resize", handleResize);

return () => {
clearTimeout(timer);
timers.forEach(t => clearTimeout(t));
window.removeEventListener("resize", handleResize);
};
}, [tags]);
Expand Down Expand Up @@ -154,7 +162,7 @@ const TagsRenderer = ({ tags }: { tags?: Array<string | BadgeItem> }) => {
);

return (
<div ref={containerRef} className="flex flex-wrap gap-1 w-full">
<div ref={containerRef} className="inline-flex gap-1" style={{ overflow: 'hidden', flexWrap: 'nowrap', maxWidth: '100%' }}>
{visibleTags.map((tag, index) => (
<Tag
key={index}
Expand All @@ -164,6 +172,7 @@ const TagsRenderer = ({ tags }: { tags?: Array<string | BadgeItem> }) => {
? undefined
: { background: tag.background, color: tag.color }
}
className="shrink-0"
>
{typeof tag === "string" ? tag : (tag.label ? tag.label : tag.name)}
</Tag>
Expand All @@ -175,7 +184,7 @@ const TagsRenderer = ({ tags }: { tags?: Array<string | BadgeItem> }) => {
trigger="hover"
placement="topLeft"
>
<Tag className="cursor-pointer bg-gray-100 border-gray-300 text-gray-600 hover:bg-gray-200">
<Tag className="cursor-pointer bg-gray-100 border-gray-300 text-gray-600 hover:bg-gray-200 shrink-0">
+{hiddenTags.length}
</Tag>
</Popover>
Expand Down Expand Up @@ -246,19 +255,6 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
>
{item?.name}
</h3>
{(item?.tags?.length ?? 0) > 0 &&
item.tags[0] &&
typeof item.tags[0] !== "string" && (
<Tag
color={item.tags[0].color}
style={{
background: item.tags[0].background,
color: item.tags[0].color,
}}
>
{item.tags[0].label ? item.tags[0].label : item.tags[0].name}
</Tag>
)}
{item?.status && (
<Tag color={item?.status?.color}>
<div className="flex items-center gap-2 text-xs py-0.5">
Expand Down Expand Up @@ -294,25 +290,17 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {

<div className="flex-1 flex flex-col justify-end">
{/* Tags */}
<TagsRenderer
tags={
Array.isArray(item?.tags)
? item.tags.filter((tag, index) =>
typeof tag === "string" || index !== 0
)
: []
}
/>
<TagsRenderer tags={Array.isArray(item?.tags) ? item.tags : []} />

{/* Description */}
<p className="text-gray-400 text-xs text-ellipsis overflow-hidden whitespace-nowrap line-clamp-2 mt-1 mb-1">
<p className="text-gray-400 text-xs text-ellipsis overflow-hidden whitespace-nowrap line-clamp-2 mt-3 mb-2">
<Tooltip title={item?.description}>
{item?.description}
</Tooltip>
</p>

{/* Statistics */}
<div className="grid grid-cols-2 gap-4 py-3">
<div className="grid grid-cols-2 gap-4 py-2">
{item?.statistics?.map((stat, idx) => (
<div key={idx}>
<div className="text-sm text-gray-500 overflow-hidden whitespace-nowrap text-ellipsis w-full">
Expand Down
Loading
Loading