277 lines
8.5 KiB
TypeScript
277 lines
8.5 KiB
TypeScript
import { View, Text, Image } from "@tarojs/components";
|
||
import Taro from "@tarojs/taro";
|
||
import { useMemo } from "react";
|
||
import img from "../../config/images";
|
||
import { ListCardProps } from "../../../types/list/types";
|
||
import { formatGameTime, calculateDuration } from "@/utils/timeUtils";
|
||
import { navigateTo } from "@/utils/navigation";
|
||
import images from "@/config/images";
|
||
import "./index.scss";
|
||
import { OSS_BASE } from "@/config/api";
|
||
|
||
const ListCard: React.FC<ListCardProps> = ({
|
||
id,
|
||
title,
|
||
original_start_time,
|
||
start_time,
|
||
end_time,
|
||
location,
|
||
distance_km,
|
||
current_players,
|
||
max_players,
|
||
skill_level_min,
|
||
skill_level_max,
|
||
play_type,
|
||
image_list = [],
|
||
court_type,
|
||
key,
|
||
participants, // 参与者图片
|
||
venue_image_list, // 场馆图片
|
||
venue_description,
|
||
game_type, // 球局类型
|
||
}) => {
|
||
// 参与者要前三个数据
|
||
const participantsImageList = participants?.slice(0, 3) || [];
|
||
// 场地 要第一个数据
|
||
// const venueImageList = venue_image_list?.slice(0, 1) || [];
|
||
const venueImage = venue_image_list?.[0]?.url || "";
|
||
|
||
// 是否显示畅打球局
|
||
const isShowSmoothPlayingGame = game_type === "畅打球局";
|
||
|
||
const renderItemImage = (src: string) => {
|
||
return (
|
||
<Image
|
||
src={src}
|
||
className="image"
|
||
mode="aspectFill"
|
||
lazyLoad
|
||
defaultSource={`${OSS_BASE}/front/ball/images/publish-empty-card.png`}
|
||
/>
|
||
);
|
||
};
|
||
|
||
const handleViewDetail = () => {
|
||
navigateTo({
|
||
url: `/game_pages/detail/index?id=${id}&from=list`,
|
||
});
|
||
};
|
||
|
||
// 处理地点截断,确保固定信息始终显示
|
||
const displayLocation = useMemo(() => {
|
||
if (!location) return null; // 直接返回 null,不渲染
|
||
|
||
// 获取屏幕宽度,用于计算实际容器宽度
|
||
const systemInfo = Taro.getSystemInfoSync();
|
||
const screenWidth = systemInfo.windowWidth || 375;
|
||
// 容器宽度 = 屏幕宽度 - 左右 padding - 图片区域宽度
|
||
const containerWidthPx = screenWidth - 130;
|
||
|
||
// 计算固定信息宽度
|
||
const extraInfo = `${court_type ? `・${court_type}` : ""}${
|
||
distance_km ? `・${distance_km}km` : ""
|
||
}`;
|
||
|
||
// 估算字符宽度(基于 12px 字体)
|
||
const getTextWidth = (text: string) => {
|
||
let width = 0;
|
||
for (let char of text) {
|
||
if (/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char)) {
|
||
width += 12; // 中文字符 12px
|
||
} else {
|
||
width += 6; // 英文字符和数字 6px
|
||
}
|
||
}
|
||
return width;
|
||
};
|
||
|
||
const extraWidth = getTextWidth(extraInfo);
|
||
const locationWidth = getTextWidth(location);
|
||
|
||
// 可用宽度 = 容器宽度 - 固定信息宽度 - 省略号宽度(18px)
|
||
const availableWidth = containerWidthPx - extraWidth - 18;
|
||
|
||
// 如果地点宽度小于可用宽度,不需要截断
|
||
if (locationWidth <= availableWidth) {
|
||
return location;
|
||
}
|
||
|
||
// 需要截断地点
|
||
let maxChars = 0;
|
||
let currentWidth = 0;
|
||
for (let i = 0; i < location.length; i++) {
|
||
const char = location[i];
|
||
const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char)
|
||
? 12
|
||
: 6;
|
||
if (currentWidth + charWidth > availableWidth) {
|
||
break;
|
||
}
|
||
currentWidth += charWidth;
|
||
maxChars++;
|
||
}
|
||
|
||
return location.slice(0, maxChars) + "...";
|
||
}, [location, court_type, distance_km]);
|
||
|
||
// 根据图片数量决定展示样式
|
||
const renderImages = () => {
|
||
if (image_list?.length === 0) return null;
|
||
|
||
if (image_list?.length === 1) {
|
||
return (
|
||
<View className="single-image">
|
||
<View className="image-container">
|
||
{renderItemImage(image_list?.[0])}
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
if (image_list?.length === 2) {
|
||
return (
|
||
<View className="double-image">
|
||
<View className="image-container">
|
||
{renderItemImage(image_list?.[1])}
|
||
</View>
|
||
<View className="image-container">
|
||
{renderItemImage(image_list?.[0])}
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
// 3张或更多图片
|
||
return (
|
||
<View className="triple-image">
|
||
<View className="image-container">
|
||
{renderItemImage(image_list?.[0])}
|
||
</View>
|
||
<View className="image-container">
|
||
{renderItemImage(image_list?.[1])}
|
||
</View>
|
||
<View className="image-container">
|
||
{renderItemImage(image_list?.[2])}
|
||
</View>
|
||
</View>
|
||
);
|
||
};
|
||
return (
|
||
<View className="listCard" key={key}>
|
||
<View className="listItem" onClick={handleViewDetail}>
|
||
{/* 左侧内容区域 */}
|
||
<View className="content">
|
||
{/* 标题 */}
|
||
{title && (
|
||
<View className="titleWrapper">
|
||
<Text className="title">{title}</Text>
|
||
<Image
|
||
src={img.ICON_LIST_RIGHT_ARROW}
|
||
className="title-right-arrow"
|
||
mode="aspectFit"
|
||
/>
|
||
</View>
|
||
)}
|
||
|
||
{/* 时间信息 */}
|
||
<View className="date-time">
|
||
<Text>{formatGameTime(original_start_time || start_time)}</Text>
|
||
{/* 时长 如 2小时 */}
|
||
{end_time && (
|
||
<Text>
|
||
{" "}
|
||
{calculateDuration(original_start_time || start_time, end_time)}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
|
||
{/* 地点,室内外,距离 */}
|
||
|
||
<View className="location">
|
||
{displayLocation && (
|
||
<Text className="location-text location-position">
|
||
{displayLocation}
|
||
</Text>
|
||
)}
|
||
<Text className="location-text location-time-distance">
|
||
{court_type && `・${court_type}`}
|
||
{distance_km && `・${distance_km}km`}
|
||
</Text>
|
||
</View>
|
||
|
||
{/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */}
|
||
<View className="bottom-info">
|
||
{participantsImageList && participantsImageList.length > 0 && (
|
||
<View className="left-section">
|
||
<View className="avatar-group">
|
||
{participantsImageList.map((item, index) => (
|
||
<View key={index} className="avatar">
|
||
<Image
|
||
className="avatar-image"
|
||
src={item?.user?.avatar_url}
|
||
mode="aspectFill"
|
||
/>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
<View className="tags">
|
||
<View className="tag">
|
||
<Text className="tag-text">
|
||
已加入 {current_players}/
|
||
<Text className="tag-text-max">{max_players}</Text>
|
||
</Text>
|
||
</View>
|
||
<View className="tag ntprTag">
|
||
<Image src={images.ICON_LIST_NTPR} className="ntprIcon" />
|
||
<Text className="tag-text">
|
||
{Number(skill_level_min)?.toFixed(1)} -{" "}
|
||
{Number(skill_level_max)?.toFixed(1)}
|
||
</Text>
|
||
{/* 分割线 */}
|
||
<View className="typeLine" />
|
||
<Text className="tag-text">{play_type}</Text>
|
||
</View>
|
||
{/* {play_type && (
|
||
<View className="tag">
|
||
<Text className="tag-text">{play_type}</Text>
|
||
</View>
|
||
)} */}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 右侧图片区域 */}
|
||
<View className="image-section">{renderImages()}</View>
|
||
</View>
|
||
{/* 畅打球局 */}
|
||
{isShowSmoothPlayingGame && (
|
||
<View className="smoothPlayingGame">
|
||
<View className="smoothWrapper">
|
||
<Image
|
||
className="iconListPlayingGame"
|
||
src={img.ICON_LIST_CHANGDA_QIuju}
|
||
mode="widthFix"
|
||
/>
|
||
{/* <Text className="smoothTitle">{game_type}</Text> */}
|
||
</View>
|
||
{venue_description && <View className="line" />}
|
||
{venue_description && (
|
||
<View className="localAreaContainer">
|
||
<View className="localAreaTitle">场馆方:</View>
|
||
<View className="localAreaWrapper">
|
||
<Image className="localArea" src={venueImage} />
|
||
<Text className="localAreaText">{venue_description}</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
export default ListCard;
|