zhaoweijie f9763778fa refactor(components): 重构碘搜索相关组件
- 优化 IodRelevant 组件中的加载状态和计数器动画
- 重构 PlaygroundIod 组件,使用新的 PlaygroundIodProvider 组件
- 改进 Context 提供的数据结构,使用更准确的命名
2025-08-24 13:04:43 +08:00

460 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useMemo } from "react"
import { Avatar, Card } from "antd"
import { AnimatePresence, motion } from "framer-motion" // 使用 CSS-in-JS 方式
import styled, { keyframes } from "styled-components"
import CountUp from "react-countup"
import { TalentPoolIcon } from "@/components/Icons/TalentPool .tsx"
import { ResearchPaperIcon } from "@/components/Icons/ResearchPaper.tsx"
import { DataProjectIcon } from "@/components/Icons/DataProject.tsx"
import { DatasetIcon } from "@/components/Icons/Dataset.tsx"
import { TechCompanyIcon } from "@/components/Icons/TechCompany.tsx"
import { ResearchInstitutesIcon } from "@/components/Icons/ResearchInstitutes.tsx"
import { NSDCIcon } from "@/components/Icons/NSDC.tsx"
const rotate = keyframes`
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
`
const breathe = keyframes`
0% {
box-shadow: 0 0 5px rgba(37, 231, 232, 0.3);
}
50% {
box-shadow: 0 0 20px rgba(37, 231, 232, 0.8);
}
100% {
box-shadow: 0 0 5px rgba(37, 231, 232, 0.3);
}
`
// 花瓣
const CircleElement = styled.div<{ delay: number; playing: boolean }>`
position: absolute;
width: 300px;
height: 160px;
background: #3b82f6; // blue-500
opacity: 0.2;
border-radius: 50%;
top: 55%;
left: 50%;
animation:
${rotate} 6s linear infinite,
${breathe} 2s infinite alternate;
animation-delay: ${(props) => props.delay}s;
animation-play-state: ${(props) => (props.playing ? "running" : "paused")};
animation-duration: 3s; /* 添加动画总持续时间 */
animation-fill-mode: forwards; /* 保持动画结束时的状态 */
`
const FrostedGlassCard = styled(Card)`
background: rgba(255, 255, 255, 0.25) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
`
const SuccessIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-green-500"
ref={ref}
{...props}>
<path
d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
})
const LoadingIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon animate-spin"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="29588"
ref={ref}
{...props}>
<path
d="M483.712 888.064a52.437333 52.437333 0 1 1 52.48 52.352 52.394667 52.394667 0 0 1-52.48-52.352z m-235.434667-53.76a65.578667 65.578667 0 1 1 46.421334 19.242667 65.962667 65.962667 0 0 1-46.378667-19.242667z m499.584-16.597333a41.984 41.984 0 0 1 59.264-59.434667 42.282667 42.282667 0 0 1 0 59.434667 41.941333 41.941333 0 0 1-59.264 0zM112.853333 546.602667a81.92 81.92 0 1 1 81.92 81.92 81.834667 81.834667 0 0 1-81.92-81.877334z m731.008 0a33.536 33.536 0 1 1 33.493334 33.578666 33.578667 33.578667 0 0 1-33.450667-33.536zM222.208 377.6a102.4 102.4 0 1 1 72.533333 29.866667 102.869333 102.869333 0 0 1-72.533333-29.824z m536.32-53.504a26.666667 26.666667 0 1 1 18.816 7.936 26.368 26.368 0 0 1-18.773333-7.893333zM414.378667 205.184a121.642667 121.642667 0 1 1 121.813333 121.6A121.728 121.728 0 0 1 414.378667 205.226667z"
p-id="29589"
fill="#4284f6"></path>
</svg>
)
})
const SearchIcon = () => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2585"
width="22px"
height="22px">
<path
d="M913.365333 842.794667l-188.16-188.16a347.648 347.648 0 0 0 69.674667-209.194667c0-192.682667-156.757333-349.44-349.44-349.44s-349.44 156.757333-349.44 349.44 156.757333 349.44 349.44 349.44a347.648 347.648 0 0 0 209.152-69.674667l188.16 188.16a49.962667 49.962667 0 0 0 70.613333-70.570666zM195.84 445.44a249.6 249.6 0 1 1 249.6 249.6 249.898667 249.898667 0 0 1-249.6-249.6z"
fill="#08307f"
p-id="2586"></path>
</svg>
)
}
// 自定义统计卡片组件
const StatCard: React.FC<{
number: number
unit?: string
label: string
decimals?: number
icon: React.ReactNode
}> = ({ number, unit, label, decimals, icon }) => {
return (
<div
className="flex flex-col items-center justify-center p-3 rounded-xl shadow-sm bg-[rgba(240,245,255,0.3)] backdrop-blur-sm border border-[rgba(200,220,255,0.25)]
">
<Avatar size={40} className="!bg-[#3581e3b3]" icon={icon} />
<div className="text-lg font-bold text-[#f00000]">
<CountUp
end={number}
duration={2.5}
separator=","
decimals={decimals}
/>
{unit}
</div>
<div className="text-sm text-[#3581e3] mt-1 flex items-center gap-2">
{" "}
{label}
</div>
</div>
)
}
export const StatisticGrid: React.FC = () => {
return (
<div className="p-6">
{/* 第一行3 个卡片 */}
<div className="grid grid-cols-3 gap-6 mb-6">
<StatCard
icon={<NSDCIcon className="w-6 h-6 text-white" color="#3581e3" />}
number={11}
unit="家"
label="国家科学数据中心"
/>
<StatCard
icon={
<ResearchInstitutesIcon
className="w-6 h-6 text-white"
color="#3581e3"
/>
}
number={763}
unit="家"
label="高等院校和科研机构"
/>
<StatCard
icon={
<TechCompanyIcon className="w-6 h-6 text-white" color="#3581e3" />
}
number={2.1}
decimals={1}
unit="万"
label="科技型企业"
/>
</div>
{/* 第二行4 个卡片 */}
<div className="grid grid-cols-4 gap-6">
<StatCard
icon={<DatasetIcon className="w-6 h-6 text-white" color="#3581e3" />}
number={537163}
label="数据集"
/>
<StatCard
icon={
<DataProjectIcon className="w-6 h-6 text-white" color="#3581e3" />
}
number={183729}
label="数据项目"
/>
<StatCard
icon={
<ResearchPaperIcon className="w-6 h-6 text-white" color="#3581e3" />
}
number={1380026}
label="数据论文"
/>
<StatCard
icon={
<TalentPoolIcon className="w-6 h-6 text-white" color="#3581e3" />
}
number={2}
unit="万"
label="科创人才"
/>
</div>
</div>
)
}
type Props = {
className?: string
}
export const PlaygroundIodRelevant: React.FC<Props> = ({ className }) => {
const { messages, iodLoading, currentMessageId, iodSearch } =
useMessageOption()
const showSearchData = useMemo(() => {
return iodSearch && messages.length > 0 && !iodLoading
}, [iodSearch, messages, iodLoading])
const data = useMemo(() => {
const currentMessage = messages?.find(
(message) => message.id === currentMessageId
)
const loading = (iodSearch && iodLoading)
const text = loading ? '正' : '已'
const text2 = loading ? '进行' : '完成'
const text3 = loading ? '……' : ''
const duration = loading ? 2.5 : 0
return [
{
title: (
<p className="font-extrabold">
{text}
<span className="text-[#f00000]">
<CountUp end={29} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
{" "}
<CountUp end={55} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
<CountUp decimals={1} end={53.7} duration={duration} separator="," />
</span>
{text2}{text3}
</p>
),
description: showSearchData ? (
<p>
<span className="text-green-700">
{" "}
<CountUp
end={currentMessage?.iodSources.data.total ?? 0}
duration={2.5}
separator=","
/>
{" "}
</span>
</p>
) : (
""
)
},
{
title: (
<p className="font-extrabold">
{text}
<span className="text-[#f00000]">
<CountUp end={138} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
<CountUp end={18.3} decimals={1} duration={duration} separator="," />
</span>
{text2}{text3}
</p>
),
description: showSearchData ? (
<p>
<span className="text-green-700">
{" "}
<CountUp
end={currentMessage?.iodSources.scenario.total ?? 0}
duration={2.5}
separator=","
/>
{" "}
</span>
</p>
) : (
""
)
},
{
title: (
<p className="font-extrabold">
{text}
<span className="text-[#f00000]">
<CountUp end={763} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
{" "}
<CountUp end={2.1} decimals={1} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
{" "}
<CountUp end={2} duration={duration} separator="," />
</span>
{text2}{text3}
</p>
),
description: showSearchData ? (
<p>
<span className="text-green-700">
{" "}
<CountUp
end={currentMessage?.iodSources.organization.total ?? 0}
duration={2.5}
separator=","
/>
{" "}
</span>
</p>
) : (
""
)
}
]
}, [messages, iodLoading])
return (
<Card
hoverable
variant="outlined"
className={`${className} translate-y-[-2px] !bg-[#d0e6ff] shadow-md`}>
<div className="h-full flex flex-col relative">
{/* 花瓣效果 */}
<div
className={`absolute inset-0 pointer-events-none z-0 overflow-hidden ${showSearchData ? "" : ""}`}>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-64 h-64">
<CircleElement delay={0} playing={true} />
<CircleElement delay={1} playing={true} />
<CircleElement delay={2} playing={true} />
</div>
</div>
{/* Header */}
<div className="p-3">
<h2 className="text-xl font-semibold text-[#1a3c87] flex justify-center items-center">
<div className="flex items-center gap-2">
<SearchIcon />
{messages.length > 0
? "科创数联网深度搜索"
: "科创数联网连接资源"}
</div>
{/*<button className="bg-[#2563eb1a] text-[#08307f] font-medium py-1 px-3 rounded-full text-sm hover:bg-[#2563eb1a] transition-colors float-right">*/}
{/* {data.length}个结果*/}
{/*</button>*/}
</h2>
<p className="text-sm text-[#1a3c87] mt-1 text-center">
{messages.length > 0
? "下面是在科创数联网上进行深度搜索得到的相关数据、场景和团队"
: "下面是科创数联网连接的数据、场景和团队"}
</p>
</div>
{/* Content */}
<div className="space-y-2 flex-1 overflow-y-auto">
{messages.length ? (
<AnimatePresence mode="wait">
<motion.div
key="search-results"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="space-y-2 flex-1 overflow-y-auto">
{data.map((item, index) => (
<FrostedGlassCard
className="[&_.ant-card-body]:!p-3 [&_.ant-card-body]:h-full shadow-md min-h-[88px]"
key={index}>
<div
className={`flex flex-col gap-2 h-full items-start ${showSearchData ? "justify-start" : "justify-center"}`}>
<div className="flex items-center gap-2">
<div>
{iodSearch && iodLoading ? (
<LoadingIcon
width={showSearchData ? 16 : 22}
height={showSearchData ? 16 : 22}
/>
) : (
<SuccessIcon
width={showSearchData ? 16 : 22}
height={showSearchData ? 16 : 22}
/>
)}
</div>
<p
className={`text-gray-700 ${showSearchData ? "text-sm" : "text-lg"}`}>
{item.title}
</p>
</div>
{item.description && (
<div className="flex-1">
<p className="text-xs text-gray-500 mt-1 pl-7">
{item.description}
</p>
</div>
)}
</div>
</FrostedGlassCard>
))}
</motion.div>
</AnimatePresence>
) : (
<AnimatePresence mode="wait">
<motion.div
key="statistic-grid"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="flex-1 overflow-y-auto">
<StatisticGrid />
</motion.div>
</AnimatePresence>
)}
</div>
</div>
</Card>
)
}