| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- import React, { useState, useEffect, useRef } from 'react';
- import { getPortraitUrl, hasPortrait } from '@/API/patient/portraitActions';
- import { CloseOutlined, LoadingOutlined } from '@ant-design/icons';
- import { Spin } from 'antd';
- import { Task } from '@/domain/work';
- /**
- * 患者照片浮动显示组件 Props
- */
- interface PatientPortraitFloatProps {
- /** 患者数据,直接从 handleRowClick 传入 */
- patient: Task | null;
- /** 关闭回调 */
- onClose?: () => void;
- }
- /**
- * 患者照片浮动显示组件
- *
- * 功能特性:
- * 1. 浮动在页面右上角(可拖拽调整位置)
- * 2. 显示后3秒自动关闭,带动态倒计时
- * 3. 提供手动关闭按钮
- * 4. 只在有患者且有照片时显示
- * 5. 支持加载状态和错误处理
- */
- const PatientPortraitFloat: React.FC<PatientPortraitFloatProps> = ({
- patient,
- onClose
- }) => {
- const [isVisible, setIsVisible] = useState(false);
- const [imageLoading, setImageLoading] = useState(false);
- const [imageError, setImageError] = useState(false);
- const [imageUrl, setImageUrl] = useState<string>('');
- const [countdown, setCountdown] = useState(3); // 倒计时状态
- // 拖拽相关状态
- const [position, setPosition] = useState({ x: 20, y: 20 }); // 初始位置:右上角
- const [isDragging, setIsDragging] = useState(false);
- const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
- const floatRef = useRef<HTMLDivElement>(null);
- // 定时器引用
- const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
- // 关闭组件
- const handleClose = () => {
- setIsVisible(false);
- if (countdownIntervalRef.current) {
- clearInterval(countdownIntervalRef.current);
- }
- onClose?.(); // 调用父组件的关闭回调
- };
- // 监听患者变化
- useEffect(() => {
- if (patient && hasPortrait(patient.portrait_status)) {
- // 有照片,显示组件
- setIsVisible(true);
- setImageLoading(true);
- setImageError(false);
- setCountdown(3); // 重置倒计时
-
- const url = getPortraitUrl(patient.portrait_file || '');
- console.log(`得到的照片路径是: ${url}`);
- setImageUrl(url);
- // 清除旧的定时器
- if (countdownIntervalRef.current) {
- clearInterval(countdownIntervalRef.current);
- }
- // 启动倒计时定时器,每秒更新一次
- countdownIntervalRef.current = setInterval(() => {
- setCountdown((prev) => {
- if (prev <= 1) {
- handleClose();
- return 0;
- }
- return prev - 1;
- });
- }, 1000);
- } else {
- // 无照片或未选中,隐藏组件
- setIsVisible(false);
- setImageUrl('');
- if (countdownIntervalRef.current) {
- clearInterval(countdownIntervalRef.current);
- }
- }
- return () => {
- if (countdownIntervalRef.current) {
- clearInterval(countdownIntervalRef.current);
- }
- };
- }, [patient, onClose]);
- // 图片加载完成
- const handleImageLoad = () => {
- setImageLoading(false);
- setImageError(false);
- };
- // 图片加载失败
- const handleImageError = () => {
- setImageLoading(false);
- setImageError(true);
- };
- // 拖拽开始
- const handleMouseDown = (e: React.MouseEvent) => {
- // 如果点击的是关闭按钮,不触发拖拽
- if ((e.target as HTMLElement).closest('.close-btn')) {
- return;
- }
- setIsDragging(true);
- setDragStart({
- x: e.clientX - position.x,
- y: e.clientY - position.y,
- });
- e.preventDefault();
- };
- // 拖拽中
- useEffect(() => {
- const handleMouseMove = (e: MouseEvent) => {
- if (isDragging) {
- const newX = e.clientX - dragStart.x;
- const newY = e.clientY - dragStart.y;
- // 限制在窗口范围内
- const maxX = window.innerWidth - (floatRef.current?.offsetWidth || 300);
- const maxY = window.innerHeight - (floatRef.current?.offsetHeight || 300);
- setPosition({
- x: Math.max(0, Math.min(newX, maxX)),
- y: Math.max(0, Math.min(newY, maxY)),
- });
- }
- };
- const handleMouseUp = () => {
- setIsDragging(false);
- };
- if (isDragging) {
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- }
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- };
- }, [isDragging, dragStart]);
- if (!isVisible) return null;
- return (
- <div
- ref={floatRef}
- className="fixed z-[1000] bg-white rounded-lg shadow-2xl"
- style={{
- top: `${position.y}px`,
- right: `${position.x}px`,
- width: '280px',
- maxWidth: '90vw',
- cursor: isDragging ? 'grabbing' : 'grab',
- transition: isDragging ? 'none' : 'all 0.3s ease-in-out',
- userSelect: 'none',
- }}
- onMouseDown={handleMouseDown}
- >
- {/* 标题栏 */}
- <div className="flex items-center justify-between p-2 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-blue-100 rounded-t-lg">
- <div className="flex items-center space-x-2">
- <span className="text-xs font-semibold text-gray-700">患者照片</span>
- {patient && (
- <span className="text-xs text-gray-500">
- {patient.PatientName}
- </span>
- )}
- <span className="text-xs font-bold text-blue-600">
- {countdown}秒
- </span>
- </div>
- <button
- className="close-btn w-5 h-5 flex items-center justify-center rounded-full hover:bg-gray-200 transition-colors"
- onClick={handleClose}
- title="关闭"
- >
- <CloseOutlined className="text-gray-600" style={{ fontSize: '10px' }} />
- </button>
- </div>
- {/* 照片区域 */}
- <div className="p-4">
- <div className="relative w-full aspect-[3/4] bg-gray-100 rounded-lg overflow-hidden flex items-center justify-center">
- {imageLoading && (
- <div className="absolute inset-0 flex items-center justify-center bg-gray-50">
- <Spin indicator={<LoadingOutlined style={{ fontSize: 32 }} spin />} />
- </div>
- )}
-
- {imageError && !imageLoading && (
- <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-50 text-gray-400">
- <svg
- className="w-16 h-16 mb-2"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={1.5}
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
- />
- </svg>
- <span className="text-sm">照片加载失败</span>
- </div>
- )}
- {imageUrl && !imageError && (
- <img
- src={imageUrl}
- alt="患者照片"
- className="w-full h-full object-cover"
- onLoad={handleImageLoad}
- onError={handleImageError}
- style={{ display: imageLoading ? 'none' : 'block' }}
- />
- )}
- </div>
- </div>
- </div>
- );
- };
- export default PatientPortraitFloat;
|