Download vetcopilot-app/src/components/LoadingSpinner.tsx from WWsCa/VetNet-LCPS: direct link, hf CLI and curl.
- Browser
- Download file 6.3 kB
-
https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/components/LoadingSpinner.tsx
- Command line
-
hf download hf://WWsCa/VetNet-LCPS/vetcopilot-app/src/components/LoadingSpinner.tsx
-
curl -L -o LoadingSpinner.tsx https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/components/LoadingSpinner.tsx
6.3 kB
| import React from 'react' | |
| import { useTranslation } from '../i18n' | |
| /** | |
| * LoadingSpinner | |
| * | |
| * 兽医主题加载动画组件。 | |
| * 支持三种风格:paw(爪印)、heartbeat(心跳线)、dna(DNA 双螺旋) | |
| * | |
| * Props: | |
| * size - 尺寸:'sm' (24px) / 'md' (40px) / 'lg' (64px) | |
| * variant - 风格:'paw' / 'heartbeat' / 'dna' / 'spinner'(默认 paw) | |
| * label - 无障碍文本,默认 "加载中..." | |
| * className - 额外的包裹层 class | |
| * colorClass - 自定义颜色类(如 'text-blue-500') | |
| */ | |
| export type SpinnerSize = 'sm' | 'md' | 'lg' | |
| export type SpinnerVariant = 'paw' | 'heartbeat' | 'dna' | 'spinner' | |
| interface LoadingSpinnerProps { | |
| size?: SpinnerSize | |
| variant?: SpinnerVariant | |
| label?: string | |
| className?: string | |
| colorClass?: string | |
| } | |
| const SIZE_MAP: Record<SpinnerSize, number> = { | |
| sm: 24, | |
| md: 40, | |
| lg: 64, | |
| } | |
| /** 爪印动画 SVG */ | |
| function PawSpinner({ size, colorClass }: { size: number; colorClass: string }) { | |
| const scale = size / 48 | |
| return ( | |
| <svg | |
| className={`animate-spin-slow ${colorClass}`} | |
| width={size} | |
| height={size} | |
| viewBox="0 0 48 48" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| style={{ transform: `scale(${scale})`, transformOrigin: 'center' }} | |
| > | |
| <g style={{ animation: 'paw-bounce 1.2s ease-in-out infinite' }}> | |
| {/* 主肉垫 */} | |
| <ellipse cx="24" cy="30" rx="10" ry="8" fill="currentColor" opacity="0.9" /> | |
| {/* 四个趾垫 */} | |
| <ellipse cx="14" cy="18" rx="4.5" ry="5" fill="currentColor" opacity="0.9" /> | |
| <ellipse cx="34" cy="18" rx="4.5" ry="5" fill="currentColor" opacity="0.9" /> | |
| <ellipse cx="20" cy="13" rx="4" ry="5" fill="currentColor" opacity="0.9" /> | |
| <ellipse cx="28" cy="13" rx="4" ry="5" fill="currentColor" opacity="0.9" /> | |
| </g> | |
| </svg> | |
| ) | |
| } | |
| /** 心跳线动画 SVG */ | |
| function HeartbeatSpinner({ size, colorClass }: { size: number; colorClass: string }) { | |
| return ( | |
| <svg | |
| width={size} | |
| height={size} | |
| viewBox="0 0 48 48" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| > | |
| {/* 心跳线路径 */} | |
| <path | |
| className={colorClass} | |
| d="M4 24h6l4-10 6 28 4-18h6" | |
| stroke="currentColor" | |
| strokeWidth="2.5" | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| fill="none" | |
| strokeDasharray="60" | |
| strokeDashoffset="60" | |
| style={{ | |
| animation: 'heartbeat-draw 1.6s ease-in-out infinite', | |
| }} | |
| /> | |
| {/* 脉冲点 */} | |
| <circle | |
| cx="24" | |
| cy="24" | |
| r="2" | |
| fill="currentColor" | |
| className={colorClass} | |
| style={{ | |
| animation: 'heartbeat-pulse 1.6s ease-in-out infinite', | |
| }} | |
| /> | |
| </svg> | |
| ) | |
| } | |
| /** DNA 双螺旋动画 SVG */ | |
| function DnaSpinner({ size, colorClass }: { size: number; colorClass: string }) { | |
| return ( | |
| <svg | |
| width={size} | |
| height={size} | |
| viewBox="0 0 48 48" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| > | |
| {/* 左侧螺旋 */} | |
| <path | |
| className={colorClass} | |
| d="M18 4c-2 6 5 14 6 20-1 6-8 14-6 20" | |
| stroke="currentColor" | |
| strokeWidth="2" | |
| strokeLinecap="round" | |
| fill="none" | |
| opacity="0.6" | |
| style={{ animation: 'dna-wave 2s ease-in-out infinite' }} | |
| /> | |
| {/* 右侧螺旋(相位偏移) */} | |
| <path | |
| className={colorClass} | |
| d="M30 4c2 6-5 14-6 20 1 6 8 14 6 20" | |
| stroke="currentColor" | |
| strokeWidth="2" | |
| strokeLinecap="round" | |
| fill="none" | |
| opacity="0.6" | |
| style={{ animation: 'dna-wave 2s ease-in-out 0.5s infinite' }} | |
| /> | |
| {/* 横杠 */} | |
| <line x1="20" y1="12" x2="28" y2="12" stroke="currentColor" strokeWidth="1.5" className={colorClass} opacity="0.5" style={{ animation: 'dna-bar 2s ease-in-out infinite' }} /> | |
| <line x1="18" y1="22" x2="30" y2="22" stroke="currentColor" strokeWidth="1.5" className={colorClass} opacity="0.5" style={{ animation: 'dna-bar 2s ease-in-out 0.3s infinite' }} /> | |
| <line x1="18" y1="32" x2="30" y2="32" stroke="currentColor" strokeWidth="1.5" className={colorClass} opacity="0.5" style={{ animation: 'dna-bar 2s ease-in-out 0.6s infinite' }} /> | |
| </svg> | |
| ) | |
| } | |
| /** 默认圆形旋转器 */ | |
| function DefaultSpinner({ size, colorClass }: { size: number; colorClass: string }) { | |
| const borderWidth = size <= 24 ? 3 : 4 | |
| return ( | |
| <div | |
| className={`rounded-full animate-spin border-gray-200 ${colorClass}`} | |
| style={{ | |
| width: size, | |
| height: size, | |
| borderWidth, | |
| borderTopColor: 'currentColor', | |
| }} | |
| role="presentation" | |
| /> | |
| ) | |
| } | |
| export default function LoadingSpinner({ | |
| size = 'md', | |
| variant = 'paw', | |
| label, | |
| className = '', | |
| colorClass = 'text-medical-600', | |
| }: LoadingSpinnerProps) { | |
| const { t } = useTranslation() | |
| const pxSize = SIZE_MAP[size] | |
| const renderSpinner = () => { | |
| switch (variant) { | |
| case 'paw': | |
| return <PawSpinner size={pxSize} colorClass={colorClass} /> | |
| case 'heartbeat': | |
| return <HeartbeatSpinner size={pxSize} colorClass={colorClass} /> | |
| case 'dna': | |
| return <DnaSpinner size={pxSize} colorClass={colorClass} /> | |
| case 'spinner': | |
| default: | |
| return <DefaultSpinner size={pxSize} colorClass={colorClass} /> | |
| } | |
| } | |
| return ( | |
| <div | |
| className={`inline-flex flex-col items-center justify-center gap-2 ${className}`} | |
| role="status" | |
| aria-label={label || undefined} | |
| > | |
| {renderSpinner()} | |
| {label && ( | |
| <span className="text-xs text-gray-400 font-medium">{label}</span> | |
| )} | |
| {!label && ( | |
| <span className="sr-only">{t('common.loading')}</span> | |
| )} | |
| </div> | |
| ) | |
| } | |
| /** | |
| * 全屏加载覆盖层 | |
| * 用于页面级加载状态 | |
| */ | |
| export function LoadingOverlay({ | |
| message = '加载中...', | |
| variant = 'paw' as SpinnerVariant, | |
| }: { | |
| message?: string | |
| variant?: SpinnerVariant | |
| }) { | |
| return ( | |
| <div | |
| className="fixed inset-0 z-[9998] flex items-center justify-center bg-white/60 backdrop-blur-sm" | |
| role="alert" | |
| aria-busy="true" | |
| > | |
| <LoadingSpinner size="lg" variant={variant} label={message} /> | |
| </div> | |
| ) | |
| } | |