feat: home page
This commit is contained in:
298
src/components/home/ip-info-card.tsx
Normal file
298
src/components/home/ip-info-card.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Skeleton,
|
||||
IconButton,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
LocationOnOutlined,
|
||||
RefreshOutlined,
|
||||
VisibilityOutlined,
|
||||
VisibilityOffOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
import { getIpInfo } from "@/services/api";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// 定义刷新时间(秒)
|
||||
const IP_REFRESH_SECONDS = 300;
|
||||
|
||||
// IP信息卡片组件
|
||||
export const IpInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [ipInfo, setIpInfo] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [showIp, setShowIp] = useState(false);
|
||||
const [countdown, setCountdown] = useState(IP_REFRESH_SECONDS);
|
||||
|
||||
// 获取IP信息
|
||||
const fetchIpInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const data = await getIpInfo();
|
||||
setIpInfo(data);
|
||||
setCountdown(IP_REFRESH_SECONDS);
|
||||
} catch (err: any) {
|
||||
setError(err.message || t("Failed to get IP info"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
// 组件加载时获取IP信息
|
||||
useEffect(() => {
|
||||
fetchIpInfo();
|
||||
}, [fetchIpInfo]);
|
||||
|
||||
// 倒计时自动刷新
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
fetchIpInfo();
|
||||
return IP_REFRESH_SECONDS;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [fetchIpInfo]);
|
||||
|
||||
// 刷新按钮点击处理
|
||||
const handleRefresh = () => {
|
||||
fetchIpInfo();
|
||||
};
|
||||
|
||||
// 切换显示/隐藏IP
|
||||
const toggleShowIp = () => {
|
||||
setShowIp(!showIp);
|
||||
};
|
||||
|
||||
// 获取国旗表情
|
||||
const getCountryFlag = (countryCode: string) => {
|
||||
if (!countryCode) return "";
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split("")
|
||||
.map((char) => 127397 + char.charCodeAt(0));
|
||||
return String.fromCodePoint(...codePoints);
|
||||
};
|
||||
|
||||
// 信息项组件 - 默认不换行,但在需要时可以换行
|
||||
const InfoItem = ({ label, value }: { label: string; value: string }) => (
|
||||
<Box sx={{ mb: 0.7, display: "flex", alignItems: "flex-start" }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
minwidth: 60,
|
||||
mr: 0.5,
|
||||
flexShrink: 0,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{label}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1, // 让内容占用剩余空间
|
||||
}}
|
||||
>
|
||||
{value || t("Unknown")}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("IP Information")}
|
||||
icon={<LocationOnOutlined />}
|
||||
iconColor="info"
|
||||
action={
|
||||
<IconButton size="small" onClick={handleRefresh} disabled={loading}>
|
||||
<RefreshOutlined />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Box sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{loading ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
<Skeleton variant="text" width="60%" height={34} />
|
||||
<Skeleton variant="text" width="80%" height={24} />
|
||||
<Skeleton variant="text" width="70%" height={24} />
|
||||
<Skeleton variant="text" width="50%" height={24} />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
color: "error.main",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="error">
|
||||
{error}
|
||||
</Typography>
|
||||
<Button onClick={handleRefresh} sx={{ mt: 2 }}>
|
||||
{t("Retry")}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 左侧:国家和IP地址 */}
|
||||
<Box sx={{ width: "40%", overflow: "hidden" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
mb: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: "1.5rem",
|
||||
mr: 1,
|
||||
display: "inline-block",
|
||||
width: 28,
|
||||
textAlign: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{getCountryFlag(ipInfo?.country_code)}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontWeight: "medium",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{ipInfo?.country || t("Unknown")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
{t("IP")}:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
ml: 1,
|
||||
overflow: "hidden",
|
||||
maxWidth: "calc(100% - 30px)",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.75rem",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
{showIp ? ipInfo?.ip : "••••••••••"}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={toggleShowIp}>
|
||||
{showIp ? (
|
||||
<VisibilityOffOutlined fontSize="small" />
|
||||
) : (
|
||||
<VisibilityOutlined fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<InfoItem
|
||||
label={t("ASN")}
|
||||
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:组织、ISP和位置信息 */}
|
||||
<Box sx={{ width: "60%", overflow: "auto" }}>
|
||||
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
|
||||
|
||||
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
||||
|
||||
<InfoItem
|
||||
label={t("Location")}
|
||||
value={[ipInfo?.city, ipInfo?.region]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
/>
|
||||
|
||||
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: "auto",
|
||||
pt: 0.5,
|
||||
borderTop: 1,
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
opacity: 0.7,
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
{t("Auto refresh")}: {countdown}s
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{ipInfo?.country_code}, {ipInfo?.longitude?.toFixed(2)},{" "}
|
||||
{ipInfo?.latitude?.toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</EnhancedCard>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user