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" ;
33import {
44 EllipsisOutlined ,
55 ClockCircleOutlined ,
66 StarFilled ,
77} from "@ant-design/icons" ;
88import type { ItemType } from "antd/es/menu/interface" ;
9+ import { formatDateTime } from "@/utils/unit" ;
910
1011interface 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+
48156function 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" >
0 commit comments