Parcourir la source

✨ feat(mobile): 添加PDF预览组件并优化知识详情页

- 新增PdfPreview组件,支持移动端PDF预览功能
- 实现浏览器兼容性检测,针对微信/QQ内置浏览器提供下载方案
- 添加响应式设计,预览高度为屏幕高度35%(最大320px)
- 优化知识详情页的React Query实现,使用v5 API重构数据获取
- 移除详情页中重复的PDF处理逻辑,使用新组件统一管理预览状态
- 添加加载状态、错误处理和兼容性提示,提升用户体验
yourname il y a 7 mois
Parent
commit
684cb89429

+ 211 - 0
src/client/mobile/components/PdfPreview.tsx

@@ -0,0 +1,211 @@
+import React, { useState, useEffect } from 'react';
+
+interface PdfPreviewProps {
+  src: string;
+  title?: string;
+  className?: string;
+  style?: React.CSSProperties;
+  onLoad?: () => void;
+  onError?: () => void;
+}
+
+// 色彩系统
+const COLORS = {
+  ink: {
+    light: '#f5f3f0',
+    medium: '#d4c4a8',
+    dark: '#8b7355',
+    deep: '#3a2f26',
+  },
+  accent: {
+    red: '#a85c5c',
+  },
+  text: {
+    primary: '#2f1f0f',
+    secondary: '#5d4e3b',
+  }
+};
+
+const PdfPreview: React.FC<PdfPreviewProps> = ({
+  src,
+  title = 'PDF预览',
+  className = '',
+  style = {},
+  onLoad,
+  onError
+}) => {
+  const [pdfLoading, setPdfLoading] = useState(true);
+  const [pdfError, setPdfError] = useState(false);
+  const [showPreview, setShowPreview] = useState(true);
+
+  // 移动端PDF兼容性检测
+  const checkPdfSupport = () => {
+    const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
+    const isWechat = /MicroMessenger/i.test(navigator.userAgent);
+    const isQQ = /QQ/i.test(navigator.userAgent);
+    
+    // 禁用微信、QQ内置浏览器的PDF预览
+    if (isWechat || isQQ) {
+      return false;
+    }
+    
+    // iOS Safari支持较好,但部分版本有问题
+    if (isIOS) {
+      return navigator.userAgent.includes('Safari') &&
+             !navigator.userAgent.includes('CriOS') && // Chrome iOS
+             !navigator.userAgent.includes('FxiOS');   // Firefox iOS
+    }
+    
+    return true;
+  };
+
+  const getCompatibilityMessage = () => {
+    const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
+    const isWechat = /MicroMessenger/i.test(navigator.userAgent);
+    const isQQ = /QQ/i.test(navigator.userAgent);
+    
+    if (isWechat || isQQ) {
+      return '微信/QQ内置浏览器暂不支持PDF预览,请使用系统浏览器打开';
+    }
+    if (isIOS && !checkPdfSupport()) {
+      return '建议使用Safari浏览器获得最佳PDF预览体验';
+    }
+    return null;
+  };
+
+  const getResponsivePreviewHeight = () => {
+    const screenHeight = window.innerHeight;
+    return Math.min(320, screenHeight * 0.35); // 35%屏幕高度,最大320px
+  };
+
+  const handleDownload = () => {
+    const link = document.createElement('a');
+    link.href = src;
+    link.download = title;
+    link.target = '_blank';
+    link.rel = 'noopener noreferrer';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+  };
+
+  useEffect(() => {
+    const isSupported = checkPdfSupport();
+    setShowPreview(isSupported);
+    
+    if (isSupported) {
+      setPdfLoading(true);
+      setPdfError(false);
+    }
+  }, [src]);
+
+  const handleLoad = () => {
+    setPdfLoading(false);
+    onLoad?.();
+  };
+
+  const handleError = () => {
+    setPdfLoading(false);
+    setPdfError(true);
+    onError?.();
+  };
+
+  const message = getCompatibilityMessage();
+
+  if (!showPreview) {
+    return (
+      <div className="p-4 rounded-lg border" style={{ borderColor: COLORS.ink.medium, backgroundColor: 'rgba(255,255,255,0.5)' }}>
+        <p className="text-sm" style={{ color: COLORS.text.primary }}>当前浏览器不支持PDF预览</p>
+        <button
+          onClick={handleDownload}
+          className="text-xs mt-2 px-3 py-1 rounded"
+          style={{ backgroundColor: COLORS.ink.dark, color: 'white' }}
+        >
+          下载查看
+        </button>
+      </div>
+    );
+  }
+
+  return (
+    <div>
+      {message && (
+        <div className="p-3 rounded-lg mb-3" style={{ backgroundColor: 'rgba(255,248,220,0.8)', borderColor: COLORS.accent.red }}>
+          <p className="text-xs" style={{ color: COLORS.accent.red }}>
+            {message}
+          </p>
+        </div>
+      )}
+
+      <div className="flex items-center justify-between mb-2">
+        <p className="text-sm" style={{ color: COLORS.text.secondary }}>
+          文件预览:
+        </p>
+        <button
+          onClick={handleDownload}
+          className="text-xs px-2 py-1 rounded"
+          style={{
+            backgroundColor: COLORS.ink.medium,
+            color: COLORS.text.primary
+          }}
+        >
+          下载查看
+        </button>
+      </div>
+
+      {pdfLoading && (
+        <div className="w-full h-48 flex items-center justify-center rounded-lg border"
+          style={{ borderColor: COLORS.ink.medium, backgroundColor: 'rgba(255,255,255,0.5)' }}
+        >
+          <div className="text-center">
+            <div className="animate-spin rounded-full h-6 w-6 border-b-2 mx-auto"
+              style={{ borderColor: COLORS.ink.dark }}
+            ></div>
+            <p className="text-xs mt-2" style={{ color: COLORS.text.secondary }}>
+              PDF加载中...
+            </p>
+          </div>
+        </div>
+      )}
+
+      {pdfError && (
+        <div className="w-full h-48 flex items-center justify-center rounded-lg border"
+          style={{ borderColor: COLORS.ink.medium, backgroundColor: 'rgba(255,255,255,0.5)' }}
+        >
+          <div className="text-center">
+            <p className="text-sm" style={{ color: COLORS.text.primary }}>
+              预览加载失败
+            </p>
+            <button
+              onClick={handleDownload}
+              className="text-xs mt-2 px-3 py-1 rounded"
+              style={{
+                backgroundColor: COLORS.ink.dark,
+                color: 'white'
+              }}
+            >
+              下载查看
+            </button>
+          </div>
+        </div>
+      )}
+
+      {!pdfLoading && !pdfError && (
+        <iframe
+          src={src}
+          className={`w-full rounded-lg border ${className}`}
+          style={{
+            borderColor: COLORS.ink.medium,
+            height: `${getResponsivePreviewHeight()}px`,
+            ...style
+          }}
+          title={title}
+          onLoad={handleLoad}
+          onError={handleError}
+        />
+      )}
+    </div>
+  );
+};
+
+export default PdfPreview;

+ 21 - 158
src/client/mobile/pages/SilverWisdomDetailPage.tsx

@@ -1,10 +1,10 @@
 import React, { useState } from 'react';
 import { useParams, useNavigate } from 'react-router-dom';
 import { useQuery } from '@tanstack/react-query';
-import { useSilverWisdomDetail, wisdomCategories } from '@/client/mobile/hooks/useSilverWisdomData';
 import { silverKnowledgeClient } from '@/client/api';
 import { ChevronLeftIcon, HeartIcon, BookmarkIcon, ShareIcon, ArrowDownTrayIcon, EyeIcon } from '@heroicons/react/24/outline';
 import { HeartIcon as HeartSolidIcon, BookmarkIcon as BookmarkSolidIcon } from '@heroicons/react/24/solid';
+import PdfPreview from '@/client/mobile/components/PdfPreview';
 import dayjs from 'dayjs';
 import 'dayjs/locale/zh-cn';
 
@@ -44,11 +44,22 @@ const SilverWisdomDetailPage: React.FC = () => {
   const navigate = useNavigate();
   const [isLiked, setIsLiked] = useState(false);
   const [isBookmarked, setIsBookmarked] = useState(false);
-  const [pdfLoading, setPdfLoading] = useState(false);
-  const [pdfError, setPdfError] = useState(false);
-  const [showPdfPreview, setShowPdfPreview] = useState(true);
 
-  const { data, isLoading, error } = useSilverWisdomDetail(Number(id));
+  // 使用react query v5查询知识详情
+  const { data, isLoading, error } = useQuery({
+    queryKey: ['silver-wisdom-detail', id],
+    queryFn: async () => {
+      if (!id) throw new Error('ID is required');
+      const response = await silverKnowledgeClient[":id"].$get({
+        param: { id: Number(id) }
+      });
+      if (!response.ok) {
+        throw new Error('获取知识详情失败');
+      }
+      return response.json();
+    },
+    enabled: !!id
+  });
 
   // 检查点赞和收藏状态
   const checkInteractionStatus = async () => {
@@ -119,46 +130,6 @@ const SilverWisdomDetailPage: React.FC = () => {
     return Math.ceil(wordCount / wordsPerMinute);
   };
 
-  const getCompatibilityMessage = () => {
-    const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
-    const isWechat = /MicroMessenger/i.test(navigator.userAgent);
-    const isQQ = /QQ/i.test(navigator.userAgent);
-    
-    if (isWechat || isQQ) {
-      return '微信/QQ内置浏览器暂不支持PDF预览,请使用系统浏览器打开';
-    }
-    if (isIOS && !checkPdfSupport()) {
-      return '建议使用Safari浏览器获得最佳PDF预览体验';
-    }
-    return null;
-  };
-
-  // 移动端PDF兼容性检测
-  const checkPdfSupport = () => {
-    const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
-    const isWechat = /MicroMessenger/i.test(navigator.userAgent);
-    const isQQ = /QQ/i.test(navigator.userAgent);
-    
-    // 禁用微信、QQ内置浏览器的PDF预览
-    if (isWechat || isQQ) {
-      return false;
-    }
-    
-    // iOS Safari支持较好,但部分版本有问题
-    if (isIOS) {
-      return navigator.userAgent.includes('Safari') &&
-             !navigator.userAgent.includes('CriOS') && // Chrome iOS
-             !navigator.userAgent.includes('FxiOS');   // Firefox iOS
-    }
-    
-    return true;
-  };
-
-  const getResponsivePreviewHeight = () => {
-    const screenHeight = window.innerHeight;
-    return Math.min(320, screenHeight * 0.35); // 35%屏幕高度,最大320px
-  };
-
   const handleDownload = () => {
     if (data?.attachment) {
       // 创建临时链接下载,避免直接打开可能的安全问题
@@ -193,22 +164,6 @@ const SilverWisdomDetailPage: React.FC = () => {
     }
   };
 
-  const handlePdfPreview = () => {
-    if (!data?.attachment) return;
-    
-    const isSupported = checkPdfSupport();
-    if (!isSupported) {
-      // 不支持的浏览器直接提供下载
-      setShowPdfPreview(false);
-      handleDownload();
-      return;
-    }
-    
-    setShowPdfPreview(true);
-    setPdfLoading(true);
-    setPdfError(false);
-  };
-
   // 处理加载状态
   if (isLoading) {
     return (
@@ -244,19 +199,6 @@ const SilverWisdomDetailPage: React.FC = () => {
   // 解析标签
   const tags = knowledge.tags ? String(knowledge.tags).split(',').map(tag => tag.trim()).filter(tag => tag) : [];
 
-  // 使用useEffect检查PDF支持并设置预览状态
-  React.useEffect(() => {
-    if (knowledge.attachment && getFileExtension(knowledge.attachmentName || '') === 'pdf') {
-      const isSupported = checkPdfSupport();
-      setShowPdfPreview(isSupported);
-      // 如果支持,初始化加载状态
-      if (isSupported) {
-        setPdfLoading(true);
-        setPdfError(false);
-      }
-    }
-  }, [knowledge.attachment, knowledge.attachmentName]);
-
   return (
     <div className="min-h-screen" style={{ backgroundColor: COLORS.ink.light }}>
       {/* 头部导航 */}
@@ -394,92 +336,13 @@ const SilverWisdomDetailPage: React.FC = () => {
                   </div>
                 </div>
                 
-                {/* 文件预览 - 仅对支持的格式显示 */}
+                {/* 文件预览 - 使用新的PDF预览组件 */}
                 {getFileExtension(knowledge.attachmentName || '') === 'pdf' && (
                   <div className="mt-4 border-t pt-4" style={{ borderColor: COLORS.ink.medium }}>
-                    {getCompatibilityMessage() && (
-                      <div className="p-3 rounded-lg mb-3"
-                        style={{ backgroundColor: 'rgba(255,248,220,0.8)', borderColor: COLORS.accent.red }}
-                      >
-                        <p className="text-xs" style={{ color: COLORS.accent.red }}>
-                          {getCompatibilityMessage()}
-                        </p>
-                      </div>
-                    )}
-                    
-                    {showPdfPreview && (
-                      <>
-                        <div className="flex items-center justify-between mb-2">
-                          <p className="text-sm" style={{ color: COLORS.text.secondary }}>
-                            文件预览:
-                          </p>
-                          <button
-                            onClick={handleDownload}
-                            className="text-xs px-2 py-1 rounded"
-                            style={{
-                              backgroundColor: COLORS.ink.medium,
-                              color: COLORS.text.primary
-                            }}
-                          >
-                            下载查看
-                          </button>
-                        </div>
-                        
-                        {pdfLoading && (
-                          <div className="w-full h-48 flex items-center justify-center rounded-lg border"
-                            style={{ borderColor: COLORS.ink.medium, backgroundColor: 'rgba(255,255,255,0.5)' }}
-                          >
-                            <div className="text-center">
-                              <div className="animate-spin rounded-full h-6 w-6 border-b-2 mx-auto"
-                                style={{ borderColor: COLORS.ink.dark }}
-                              ></div>
-                              <p className="text-xs mt-2" style={{ color: COLORS.text.secondary }}>
-                                PDF加载中...
-                              </p>
-                            </div>
-                          </div>
-                        )}
-                        
-                        {pdfError && (
-                          <div className="w-full h-48 flex items-center justify-center rounded-lg border"
-                            style={{ borderColor: COLORS.ink.medium, backgroundColor: 'rgba(255,255,255,0.5)' }}
-                          >
-                            <div className="text-center">
-                              <p className="text-sm" style={{ color: COLORS.text.primary }}>
-                                预览加载失败
-                              </p>
-                              <button
-                                onClick={handleDownload}
-                                className="text-xs mt-2 px-3 py-1 rounded"
-                                style={{
-                                  backgroundColor: COLORS.ink.dark,
-                                  color: 'white'
-                                }}
-                              >
-                                下载查看
-                              </button>
-                            </div>
-                          </div>
-                        )}
-                        
-                        {!pdfLoading && !pdfError && (
-                          <iframe
-                            src={knowledge.attachment}
-                            className="w-full rounded-lg border"
-                            style={{
-                              borderColor: COLORS.ink.medium,
-                              height: `${getResponsivePreviewHeight()}px`
-                            }}
-                            title={knowledge.attachmentName || 'PDF预览'}
-                            onLoad={() => setPdfLoading(false)}
-                            onError={() => {
-                              setPdfLoading(false);
-                              setPdfError(true);
-                            }}
-                          />
-                        )}
-                      </>
-                    )}
+                    <PdfPreview
+                      src={knowledge.attachment}
+                      title={knowledge.attachmentName || 'PDF预览'}
+                    />
                   </div>
                 )}
               </div>