更新依赖项,升级 expo-font 和 expo-router 版本,添加 react-native-svg 支持
This commit is contained in:
@@ -7,6 +7,7 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Image,
|
||||
LayoutChangeEvent,
|
||||
Modal,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
@@ -14,6 +15,12 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import Svg, {
|
||||
Defs,
|
||||
Path,
|
||||
Stop,
|
||||
LinearGradient as SvgLinearGradient,
|
||||
} from "react-native-svg";
|
||||
|
||||
interface StatsCardProps {
|
||||
match: LiveScoreMatch;
|
||||
@@ -25,6 +32,11 @@ export function StatsCard({ match, isDark }: StatsCardProps) {
|
||||
const [flagSwitch, setFlagSwitch] = useState(true);
|
||||
const [cardSwitch, setCardSwitch] = useState(true);
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
const [chartWidth, setChartWidth] = useState(0);
|
||||
|
||||
const onChartLayout = (event: LayoutChangeEvent) => {
|
||||
setChartWidth(event.nativeEvent.layout.width);
|
||||
};
|
||||
|
||||
// 实时时间与进度计算
|
||||
const getInitialMinutes = () => {
|
||||
@@ -94,15 +106,50 @@ export function StatsCard({ match, isDark }: StatsCardProps) {
|
||||
parseInt(possession.away.toString().replace("%", "")) || 50;
|
||||
|
||||
// 用随机数据模拟压力曲线
|
||||
const generateData = () => {
|
||||
return Array.from({ length: 90 }, (_, i) => ({
|
||||
time: i,
|
||||
home: Math.sin(i / 5) * 20 + Math.random() * 10,
|
||||
away: Math.cos(i / 4) * 15 + Math.random() * 12,
|
||||
}));
|
||||
const generateData = React.useMemo(() => {
|
||||
const data = [];
|
||||
let h = 10;
|
||||
let a = 10;
|
||||
for (let i = 0; i < 90; i++) {
|
||||
h = Math.max(0, Math.min(40, h + (Math.random() - 0.5) * 10));
|
||||
a = Math.max(0, Math.min(40, a + (Math.random() - 0.5) * 10));
|
||||
data.push({
|
||||
time: i,
|
||||
home: h,
|
||||
away: a,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}, [match.event_key]);
|
||||
|
||||
const getWavePath = (
|
||||
data: any[],
|
||||
type: "home" | "away",
|
||||
width: number,
|
||||
height: number,
|
||||
) => {
|
||||
if (width === 0) return "";
|
||||
const baseY = height / 2;
|
||||
const points = data.map((d, i) => {
|
||||
const x = (i / 89) * width;
|
||||
const val = type === "home" ? d.home : d.away;
|
||||
const y =
|
||||
type === "home"
|
||||
? baseY - (val / 50) * baseY
|
||||
: baseY + (val / 50) * baseY;
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
// 创建平滑曲线 (使用简单指令)
|
||||
let d = `M 0 ${baseY}`;
|
||||
points.forEach((p) => {
|
||||
d += ` L ${p.x} ${p.y}`;
|
||||
});
|
||||
d += ` L ${width} ${baseY} Z`;
|
||||
return d;
|
||||
};
|
||||
|
||||
const chartData = generateData();
|
||||
const chartData = generateData;
|
||||
|
||||
const iconTintColor = isDark ? "#888" : "#CCC";
|
||||
const switchBg = isDark ? "#2C2C2E" : "#F0F0F0";
|
||||
@@ -171,72 +218,120 @@ export function StatsCard({ match, isDark }: StatsCardProps) {
|
||||
<View style={styles.gridContainer}>
|
||||
{["0'", "15'", "30'", "HT", "60'", "75'", "90'"].map((label, i) => (
|
||||
<View key={i} style={styles.gridLine}>
|
||||
<View style={styles.line} />
|
||||
<ThemedText style={styles.gridLabel}>{label}</ThemedText>
|
||||
<View
|
||||
style={[styles.line, isDark && { backgroundColor: "#333" }]}
|
||||
/>
|
||||
<ThemedText
|
||||
style={[styles.gridLabel, isDark && { color: "#888" }]}
|
||||
>
|
||||
{label}
|
||||
</ThemedText>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 压力曲线模拟 (使用密集的小条形模拟波形) */}
|
||||
<View style={styles.waveContainer}>
|
||||
{chartData.map((d, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<View
|
||||
style={[
|
||||
styles.waveBar,
|
||||
{
|
||||
left: `${(i / 90) * 100}%`,
|
||||
height: Math.abs(d.home),
|
||||
bottom: "50%",
|
||||
backgroundColor: "rgba(59, 89, 152, 0.8)",
|
||||
borderTopLeftRadius: 1,
|
||||
borderTopRightRadius: 1,
|
||||
},
|
||||
]}
|
||||
{/* 压力曲线模拟 (Wave Chart) */}
|
||||
<View style={styles.waveContainer} onLayout={onChartLayout}>
|
||||
{chartWidth > 0 && (
|
||||
<Svg
|
||||
width={chartWidth}
|
||||
height={110}
|
||||
style={{ position: "absolute" }}
|
||||
>
|
||||
<Defs>
|
||||
<SvgLinearGradient
|
||||
id="homeGradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<Stop offset="0" stopColor="#3b5998" stopOpacity="0.6" />
|
||||
<Stop offset="1" stopColor="#3b5998" stopOpacity="0" />
|
||||
</SvgLinearGradient>
|
||||
<SvgLinearGradient
|
||||
id="awayGradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<Stop offset="0" stopColor="#FFAB00" stopOpacity="0" />
|
||||
<Stop offset="1" stopColor="#FFAB00" stopOpacity="0.6" />
|
||||
</SvgLinearGradient>
|
||||
</Defs>
|
||||
{/* Home Wave */}
|
||||
<Path
|
||||
d={getWavePath(chartData, "home", chartWidth, 110)}
|
||||
fill="url(#homeGradient)"
|
||||
stroke="#3b5998"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.waveBar,
|
||||
{
|
||||
left: `${(i / 90) * 100}%`,
|
||||
height: Math.abs(d.away),
|
||||
top: "50%",
|
||||
backgroundColor: "rgba(255, 171, 0, 0.8)",
|
||||
borderBottomLeftRadius: 1,
|
||||
borderBottomRightRadius: 1,
|
||||
},
|
||||
]}
|
||||
{/* Away Wave */}
|
||||
<Path
|
||||
d={getWavePath(chartData, "away", chartWidth, 110)}
|
||||
fill="url(#awayGradient)"
|
||||
stroke="#FFAB00"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Svg>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 比赛事件标记 (假点位) */}
|
||||
<View style={[styles.eventMarker, { left: "12%", top: "10%" }]}>
|
||||
<View style={[styles.square, { backgroundColor: "#FFD700" }]} />
|
||||
</View>
|
||||
<View style={[styles.eventMarker, { left: "55%", top: "10%" }]}>
|
||||
<View style={[styles.square, { backgroundColor: "#FFD700" }]} />
|
||||
</View>
|
||||
<View style={[styles.eventMarker, { left: "65%", top: "10%" }]}>
|
||||
<View style={[styles.square, { backgroundColor: "#FF4444" }]} />
|
||||
</View>
|
||||
<View style={[styles.eventMarker, { left: "52%", bottom: "25%" }]}>
|
||||
<View style={styles.goalBox}>
|
||||
<IconSymbol name="football-outline" size={10} color="#000" />
|
||||
<View
|
||||
style={[
|
||||
styles.square,
|
||||
{
|
||||
backgroundColor: "#FFD700",
|
||||
width: 8,
|
||||
height: 10,
|
||||
borderRadius: 1,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{/* 动态标记:红黄牌 */}
|
||||
{chartWidth > 0 &&
|
||||
cardSwitch &&
|
||||
match.cards?.map((card, idx) => {
|
||||
const time = parseInt(card.time) || 0;
|
||||
const left = (time / 90) * chartWidth;
|
||||
const isHome = card.home_fault !== "";
|
||||
const isYellow = (card.card || "")
|
||||
.toLowerCase()
|
||||
.includes("yellow");
|
||||
return (
|
||||
<View
|
||||
key={`card-${idx}`}
|
||||
style={[
|
||||
styles.eventMarker,
|
||||
{ left, top: isHome ? "5%" : "72%" },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.square,
|
||||
{
|
||||
backgroundColor: isYellow ? "#FFD700" : "#FF3B30",
|
||||
width: 8,
|
||||
height: 12,
|
||||
borderRadius: 1,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 动态标记:进球 */}
|
||||
{chartWidth > 0 &&
|
||||
flagSwitch &&
|
||||
match.goalscorers?.map((goal, idx) => {
|
||||
const time = parseInt(goal.time) || 0;
|
||||
const left = (time / 90) * chartWidth;
|
||||
const isHome = !!goal.home_scorer;
|
||||
return (
|
||||
<View
|
||||
key={`goal-${idx}`}
|
||||
style={[
|
||||
styles.eventMarker,
|
||||
{ left, top: isHome ? "25%" : "55%" },
|
||||
]}
|
||||
>
|
||||
<View style={styles.goalBox}>
|
||||
<IconSymbol name="football" size={10} color="#000" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user