Преглед изворни кода

📝 docs(component): 完善组件文档并优化图片加载功能

- 新增组件文档文件component-documentation.md,详细记录管理后台组件、移动端组件和通用工具方法
- 添加MinioUploader、AttachmentUploader等多个组件的使用说明和示例代码
- 新增ImageWithFallback组件,实现图片加载失败时的优雅降级和占位图显示
- 优化PolicyNewsCard组件的图片处理逻辑,支持多图片URL解析和错误处理
- 更新mock数据中的图片路径,统一使用本地占位图片
- 改进图片加载状态显示,添加加载动画和错误提示

✨ feat(mobile): 实现图片加载优化组件

- 开发ImageWithFallback组件,支持图片懒加载、加载状态显示和失败降级
- 添加图片加载进度动画,提升用户体验
- 实现错误重试机制,自动切换到备用图片
- 添加图片加载状态管理,包括加载中、成功和失败状态

♻️ refactor(mobile): 优化政策新闻卡片图片处理逻辑

- 重构getFirstImage方法,支持多图片URL解析和错误处理
- 优化图片路径处理,支持逗号分隔的多图片URL
- 改进图片错误处理机制,统一使用占位图
- 优化日期格式化和摘要截断函数,处理边界情况

🔧 chore(assets): 添加本地占位图片资源

- 添加placeholder-banner.jpg作为默认占位图片
- 更新所有组件示例中的图片路径,使用本地资源
- 统一管理占位图片资源,提高加载速度
yourname пре 7 месеци
родитељ
комит
177f14be70

+ 503 - 0
docs/component-documentation.md

@@ -0,0 +1,503 @@
+# 项目组件文档
+
+## 目录
+1. [管理后台组件](#管理后台组件)
+2. [移动端组件](#移动端组件)
+3. [通用工具方法](#通用工具方法)
+4. [使用示例](#使用示例)
+
+---
+
+## 管理后台组件
+
+### 1. MinioUploader - MinIO文件上传组件
+
+**位置**: `src/client/admin/components/MinioUploader.tsx`
+
+**作用**: 提供完整的文件上传功能,支持拖拽上传、进度显示、多文件上传等
+
+**属性**:
+```typescript
+interface MinioUploaderProps {
+  uploadPath: string;           // 上传路径
+  accept?: string;             // 允许的文件类型
+  maxSize?: number;            // 最大文件大小(MB)
+  multiple?: boolean;          // 是否支持多文件
+  onUploadSuccess?: (fileKey: string, fileUrl: string, file: File) => void;
+  onUploadError?: (error: Error, file: File) => void;
+  buttonText?: string;         // 自定义按钮文本
+  tipText?: string;           // 自定义提示文本
+}
+```
+
+**使用方法**:
+```tsx
+<MinioUploader
+  uploadPath="/documents/"
+  accept=".pdf,.doc,.docx"
+  maxSize={10}
+  multiple={true}
+  onUploadSuccess={(fileKey, fileUrl, file) => {
+    console.log('上传成功:', fileUrl);
+  }}
+  buttonText="上传文档"
+  tipText="支持PDF、Word格式,最大10MB"
+/>
+```
+
+### 2. AttachmentUploader - 附件上传组件
+
+**位置**: `src/client/admin/components/AttachmentUploader.tsx`
+
+**作用**: 专门用于上传附件文件,支持多种文档格式
+
+**支持格式**: PDF、DOC、DOCX、XLS、XLSX、PPT、PPTX、TXT、ZIP、RAR
+
+**使用方法**:
+```tsx
+<AttachmentUploader
+  value={attachmentUrl}
+  fileName={attachmentName}
+  onChange={(url, name) => {
+    setAttachmentUrl(url);
+    setAttachmentName(name);
+  }}
+/>
+```
+
+### 3. CoverImageUploader - 封面图片上传组件
+
+**位置**: `src/client/admin/components/CoverImageUploader.tsx`
+
+**作用**: 专门用于上传封面图片,支持图片预览
+
+**支持格式**: JPG、PNG、GIF,最大5MB
+
+**使用方法**:
+```tsx
+<CoverImageUploader
+  value={coverUrl}
+  onChange={(url) => setCoverUrl(url)}
+/>
+```
+
+### 4. CompanySelect - 公司选择器
+
+**位置**: `src/client/admin/components/CompanySelect.tsx`
+
+**作用**: 从已认证公司列表中选择公司,支持搜索功能
+
+**属性**:
+```typescript
+interface CompanySelectProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  allowClear?: boolean;
+  showSearch?: boolean;
+}
+```
+
+**使用方法**:
+```tsx
+<CompanySelect
+  value={selectedCompanyId}
+  onChange={setSelectedCompanyId}
+  placeholder="选择合作公司"
+/>
+```
+
+### 5. KnowledgeCategoryTreeSelect - 知识分类树形选择器
+
+**位置**: `src/client/admin/components/KnowledgeCategoryTreeSelect.tsx`
+
+**作用**: 以树形结构选择知识分类,支持多级分类
+
+**属性**:
+```typescript
+interface KnowledgeCategoryTreeSelectProps {
+  value?: number | null;
+  onChange?: (value: number | null) => void;
+  placeholder?: string;
+  allowClear?: boolean;
+  multiple?: boolean;
+  showRoot?: boolean;
+  rootLabel?: string;
+}
+```
+
+**使用方法**:
+```tsx
+<KnowledgeCategoryTreeSelect
+  value={categoryId}
+  onChange={setCategoryId}
+  placeholder="选择知识分类"
+  showRoot={true}
+  rootLabel="全部分类"
+/>
+```
+
+### 6. StatCard - 统计卡片组件
+
+**位置**: `src/client/admin/components/StatCard.tsx`
+
+**作用**: 显示统计数据的卡片组件,带趋势指示
+
+**属性**:
+```typescript
+interface StatCardProps {
+  title: string;
+  value: number;
+  prefix?: React.ReactNode;
+  suffix?: string;
+  trend?: 'up' | 'down';
+  trendValue?: string;
+  color: string; // 'blue' | 'green' | 'orange' | 'purple' | 'red' | 'pink'
+  icon: React.ReactNode;
+}
+```
+
+**使用方法**:
+```tsx
+<StatCard
+  title="总用户数"
+  value={12580}
+  trend="up"
+  trendValue="+12.5%"
+  color="blue"
+  icon={<UserOutlined />}
+  suffix="本月新增"
+/>
+```
+
+---
+
+## 移动端组件
+
+### 1. AvatarUpload - 头像上传组件 (移动端)
+
+**位置**: `src/client/mobile/components/AvatarUpload.tsx`
+
+**作用**: 移动端头像上传,支持图片裁剪和压缩
+
+**属性**:
+```typescript
+interface AvatarUploadProps {
+  currentAvatar?: string | null;
+  onUploadSuccess?: (avatarUrl: string) => void;
+  size?: number; // 头像大小,默认96px
+}
+```
+
+**特色功能**:
+- 图片压缩
+- 圆形裁剪
+- 水墨风格UI
+- 实时预览
+
+**使用方法**:
+```tsx
+<AvatarUpload
+  currentAvatar={user.avatar}
+  onUploadSuccess={(url) => updateUserAvatar(url)}
+  size={120}
+/>
+```
+
+### 2. AvatarCropper - 头像裁剪组件
+
+**位置**: `src/client/mobile/components/AvatarCropper.tsx`
+
+**作用**: 提供图片裁剪功能,支持拖拽和缩放
+
+**属性**:
+```typescript
+interface AvatarCropperProps {
+  imageUrl: string;
+  onCrop: (croppedBlob: Blob) => void;
+  onCancel: () => void;
+  size?: number; // 裁剪区域大小
+}
+```
+
+**使用方法**:
+```tsx
+<AvatarCropper
+  imageUrl={previewImage}
+  onCrop={(blob) => handleCropComplete(blob)}
+  onCancel={() => setShowCropper(false)}
+  size={300}
+/>
+```
+
+### 3. SmartAssistant - 智能助手组件
+
+**位置**: `src/client/mobile/components/SmartAssistant/`
+
+**作用**: 智能对话助手,支持多种AI Agent
+
+**主要组件**:
+- `SmartAssistant`: 主组件
+- `ChatWindow`: 聊天窗口
+- `MessageBubble`: 消息气泡
+- `AgentSelector`: Agent选择器
+- `FloatingButton`: 悬浮按钮
+
+**使用方法**:
+```tsx
+import { SmartAssistant } from '@/client/mobile/components/SmartAssistant';
+
+<SmartAssistant />
+```
+
+---
+
+## 通用工具方法
+
+### 1. 文件上传工具 - minio.ts
+
+**位置**: `src/client/utils/minio.ts`
+
+**主要功能**:
+- MinIO文件上传
+- 分段上传大文件
+- 进度回调
+- 上传策略获取
+
+**使用方法**:
+
+#### 基础上传
+```typescript
+import { uploadFile } from '@/client/utils/minio';
+
+const url = await uploadFile(file, 'avatars');
+```
+
+#### 带进度上传
+```typescript
+import { uploadMinIOWithPolicy } from '@/client/utils/minio';
+
+const result = await uploadMinIOWithPolicy(
+  'avatars',
+  file,
+  `user-${userId}.jpg`,
+  {
+    onProgress: (event) => {
+      console.log(`进度: ${event.progress}%`);
+    }
+  }
+);
+```
+
+### 2. 图片处理工具 - upload.ts
+
+**位置**: `src/client/utils/upload.ts`
+
+**主要功能**:
+- 图片压缩
+- Base64转换
+- 文件类型检查
+- 文件大小验证
+
+**使用方法**:
+
+#### 图片压缩
+```typescript
+import { compressImage } from '@/client/utils/upload';
+
+const compressedFile = await compressImage(file, 800, 800, 0.8);
+```
+
+#### 文件验证
+```typescript
+import { isImageFile, checkFileSize } from '@/client/utils/upload';
+
+if (!isImageFile(file)) {
+  alert('请选择图片文件');
+}
+
+if (!checkFileSize(file, 2)) {
+  alert('文件大小不能超过2MB');
+}
+```
+
+### 3. API客户端 - api.ts
+
+**位置**: `src/client/api.ts`
+
+**主要功能**:
+- 统一的API调用接口
+- 类型安全的API调用
+- 自动处理认证
+
+**使用方法**:
+```typescript
+import { userClient } from '@/client/api';
+
+// 获取用户信息
+const response = await userClient[userId].$get();
+const userData = await response.json();
+
+// 更新用户信息
+await userClient[userId].$put({
+  json: { username: 'newName' }
+});
+```
+
+---
+
+## 使用示例
+
+### 1. 完整的上传流程示例
+
+```tsx
+import React, { useState } from 'react';
+import { MinioUploader } from '@/client/admin/components/MinioUploader';
+import { message } from 'antd';
+
+const DocumentUploadExample: React.FC = () => {
+  const [fileUrl, setFileUrl] = useState<string>('');
+
+  const handleUploadSuccess = (fileKey: string, fileUrl: string, file: File) => {
+    setFileUrl(fileUrl);
+    message.success(`文件 ${file.name} 上传成功`);
+  };
+
+  const handleUploadError = (error: Error, file: File) => {
+    message.error(`文件 ${file.name} 上传失败: ${error.message}`);
+  };
+
+  return (
+    <div>
+      <MinioUploader
+        uploadPath="/documents/2024/"
+        accept=".pdf,.doc,.docx"
+        maxSize={10}
+        multiple={false}
+        onUploadSuccess={handleUploadSuccess}
+        onUploadError={handleUploadError}
+        buttonText="上传文档"
+        tipText="支持PDF、Word格式,最大10MB"
+      />
+      
+      {fileUrl && (
+        <div className="mt-4">
+          <a href={fileUrl} target="_blank" rel="noopener noreferrer">
+            查看已上传文件
+          </a>
+        </div>
+      )}
+    </div>
+  );
+};
+```
+
+### 2. 移动端头像上传示例
+
+```tsx
+import React, { useState } from 'react';
+import { AvatarUpload } from '@/client/mobile/components/AvatarUpload';
+import { useAuth } from '@/client/mobile/hooks/AuthProvider';
+
+const ProfileAvatar: React.FC = () => {
+  const { user, updateUser } = useAuth();
+  const [avatar, setAvatar] = useState(user?.avatar);
+
+  const handleAvatarUpload = (avatarUrl: string) => {
+    setAvatar(avatarUrl);
+    updateUser({ avatar: avatarUrl });
+  };
+
+  return (
+    <div className="flex flex-col items-center">
+      <AvatarUpload
+        currentAvatar={avatar}
+        onUploadSuccess={handleAvatarUpload}
+        size={120}
+      />
+      <p className="mt-2 text-gray-600">点击头像更换</p>
+    </div>
+  );
+};
+```
+
+### 3. 公司选择器使用示例
+
+```tsx
+import React, { useState } from 'react';
+import { CompanySelect } from '@/client/admin/components/CompanySelect';
+
+const JobForm: React.FC = () => {
+  const [companyId, setCompanyId] = useState<number>();
+
+  const handleCompanyChange = (value: number) => {
+    setCompanyId(value);
+    // 触发其他逻辑,如加载公司详情
+  };
+
+  return (
+    <div>
+      <label>选择公司</label>
+      <CompanySelect
+        value={companyId}
+        onChange={handleCompanyChange}
+        placeholder="请选择发布职位的公司"
+      />
+    </div>
+  );
+};
+```
+
+### 4. 批量文件上传示例
+
+```tsx
+import React, { useState } from 'react';
+import { MinioUploader } from '@/client/admin/components/MinioUploader';
+import type { UploadFile } from 'antd';
+
+const BatchUploadExample: React.FC = () => {
+  const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
+
+  const handleUploadSuccess = (fileKey: string, fileUrl: string, file: File) => {
+    setUploadedFiles(prev => [...prev, fileUrl]);
+  };
+
+  return (
+    <div>
+      <h3>批量上传图片</h3>
+      <MinioUploader
+        uploadPath="/gallery/2024/"
+        accept="image/*"
+        maxSize={5}
+        multiple={true}
+        onUploadSuccess={handleUploadSuccess}
+        buttonText="选择图片"
+        tipText="支持JPG、PNG、GIF格式,每张最大5MB"
+      />
+      
+      <div className="mt-4">
+        <h4>已上传文件:</h4>
+        {uploadedFiles.map((url, index) => (
+          <img key={index} src={url} alt={`uploaded-${index}`} className="w-32 h-32" />
+        ))}
+      </div>
+    </div>
+  );
+};
+```
+
+## 注意事项
+
+1. **文件大小限制**: 不同类型的上传组件有不同的文件大小限制
+2. **文件格式验证**: 上传前务必验证文件格式和大小
+3. **进度显示**: 大文件上传建议使用进度条显示
+4. **错误处理**: 所有上传操作都应该有完善的错误处理
+5. **安全性**: 敏感文件上传需要额外的权限验证
+
+## 最佳实践
+
+1. **统一文件命名**: 使用时间戳或UUID避免文件名冲突
+2. **分目录存储**: 按类型、日期或用户分目录存储
+3. **压缩优化**: 大图片上传前先压缩
+4. **缓存清理**: 定期清理无用文件
+5. **权限控制**: 根据用户角色限制上传权限

+ 1 - 1
public/images/placeholder-banner.jpg

@@ -1 +1 @@
-<!-- Placeholder banner image -->
+data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjI1MCIgdmlld0JveD0iMCAwIDQwMCAyNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iMjUwIiBmaWxsPSIjRjVGNUY1Ii8+CjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI0MDAiIGhlaWdodD0iMjUwIiBmaWxsPSIjRjhGOEY4Ii8+CjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI0MDAiIGhlaWdodD0iMTI1IiBmaWxsPSIjRkFGQUZBIi8+CjxyZWN0IHg9IjAiIHk9IjEyNSIgd2lkdGg9IjQwMCIgaGVpZ2h0PSIxMjUiIGZpbGw9IiNGNUY1RjUiLz4KPGNpcmNsZSBjeD0iMjAwIiBjeT0iMTI1IiByPSI0MCIgZmlsbD0iI0U1RTVFNSIgc3Ryb2tlPSIjRDVEQUREIiBzdHJva2Utd2lkdGg9IjIiLz4KPHN2ZyB4PSIxNzAiIHk9Ijk1IiB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSI+CjxwYXRoIGQ9Ik0xMiAxNVY5TTEyIDlMOCAxM00xMiA5TDE2IDEzIiBzdHJva2U9IiM5Q0EzQUYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+Cjwvc3ZnPgo8L3N2Zz4=

+ 85 - 0
src/client/mobile/components/ImageWithFallback.tsx

@@ -0,0 +1,85 @@
+import React, { useState, useEffect } from 'react';
+
+interface ImageWithFallbackProps {
+  src: string;
+  alt: string;
+  className?: string;
+  fallbackSrc?: string;
+  onLoad?: () => void;
+  onError?: () => void;
+}
+
+const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
+  src,
+  alt,
+  className = '',
+  fallbackSrc = '/images/placeholder-banner.jpg',
+  onLoad,
+  onError,
+}) => {
+  const [imageSrc, setImageSrc] = useState<string>('');
+  const [isLoading, setIsLoading] = useState(true);
+  const [hasError, setHasError] = useState(false);
+
+  useEffect(() => {
+    setImageSrc(src);
+    setIsLoading(true);
+    setHasError(false);
+  }, [src]);
+
+  const handleImageLoad = () => {
+    setIsLoading(false);
+    onLoad?.();
+  };
+
+  const handleImageError = () => {
+    if (!hasError && src !== fallbackSrc) {
+      setHasError(true);
+      setImageSrc(fallbackSrc);
+      onError?.();
+    } else {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <div className={`relative ${className}`}>
+      {isLoading && (
+        <div className="absolute inset-0 flex items-center justify-center bg-gray-100">
+          <div className="animate-pulse">
+            <div className="w-8 h-8 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin"></div>
+          </div>
+        </div>
+      )}
+      
+      <img
+        src={imageSrc}
+        alt={alt}
+        className={`${className} ${isLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-300`}
+        onLoad={handleImageLoad}
+        onError={handleImageError}
+        loading="lazy"
+      />
+      
+      {hasError && imageSrc === fallbackSrc && (
+        <div className="absolute inset-0 flex items-center justify-center bg-gray-100">
+          <svg
+            className="w-12 h-12 text-gray-400"
+            fill="none"
+            stroke="currentColor"
+            viewBox="0 0 24 24"
+          >
+            <path
+              strokeLinecap="round"
+              strokeLinejoin="round"
+              strokeWidth={2}
+              d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
+            />
+          </svg>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default ImageWithFallback;

+ 27 - 16
src/client/mobile/components/PolicyNewsCard.tsx

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
 import { CalendarDaysIcon, EyeIcon } from '@heroicons/react/24/outline';
 import type { PolicyNewsItem } from '@/client/mobile/hooks/usePolicyNewsData';
 import { PLACEHOLDER_IMAGE } from '@/client/mobile/data/mockPolicyNewsData';
+import ImageWithFallback from './ImageWithFallback';
 
 // 水墨风格颜色配置
 const COLORS = {
@@ -31,6 +32,7 @@ interface PolicyNewsCardProps {
 
 const PolicyNewsCard: React.FC<PolicyNewsCardProps> = ({ news, onClick }) => {
   const navigate = useNavigate();
+  const [imageError, setImageError] = useState(false);
 
   const handleClick = () => {
     if (onClick) {
@@ -41,10 +43,13 @@ const PolicyNewsCard: React.FC<PolicyNewsCardProps> = ({ news, onClick }) => {
   };
 
   const getFirstImage = () => {
-    if (news.images) {
-      return news.images;
+    if (imageError || !news.images) {
+      return PLACEHOLDER_IMAGE;
     }
-    return PLACEHOLDER_IMAGE;
+    
+    // 处理 images 字符串,可能是单个URL或逗号分隔的多个URL
+    const images = news.images.split(',');
+    return images[0]?.trim() || PLACEHOLDER_IMAGE;
   };
 
   const formatDate = (date: string | Date) => {
@@ -56,7 +61,7 @@ const PolicyNewsCard: React.FC<PolicyNewsCardProps> = ({ news, onClick }) => {
   };
 
   const truncateSummary = (text: string, maxLength: number = 60) => {
-    if (text.length <= maxLength) return text;
+    if (!text || text.length <= maxLength) return text || '';
     return text.substring(0, maxLength) + '...';
   };
 
@@ -67,14 +72,12 @@ const PolicyNewsCard: React.FC<PolicyNewsCardProps> = ({ news, onClick }) => {
       onClick={handleClick}
     >
       {/* 图片区域 */}
-      <div className="relative">
-        <img
+      <div className="relative w-full h-40">
+        <ImageWithFallback
           src={getFirstImage()}
           alt={news.newsTitle}
-          className="w-full h-40 object-cover"
-          onError={(e) => {
-            (e.target as HTMLImageElement).src = '/images/placeholder-banner.jpg';
-          }}
+          className="w-full h-full object-cover"
+          fallbackSrc={PLACEHOLDER_IMAGE}
         />
         
         {/* 精选标记 */}
@@ -160,6 +163,7 @@ const PolicyNewsCard: React.FC<PolicyNewsCardProps> = ({ news, onClick }) => {
 // 简化版卡片(用于首页展示)
 export const PolicyNewsCardSimple: React.FC<PolicyNewsCardProps> = ({ news, onClick }) => {
   const navigate = useNavigate();
+  const [imageError, setImageError] = useState(false);
 
   const handleClick = () => {
     if (onClick) {
@@ -176,21 +180,28 @@ export const PolicyNewsCardSimple: React.FC<PolicyNewsCardProps> = ({ news, onCl
     });
   };
 
+  const getThumbnailImage = () => {
+    if (!news.images) {
+      return PLACEHOLDER_IMAGE;
+    }
+    
+    const images = news.images.split(',');
+    return images[0]?.trim() || PLACEHOLDER_IMAGE;
+  };
+
   return (
-    <div 
+    <div
       className="bg-white bg-opacity-70 backdrop-blur-sm rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 cursor-pointer border border-opacity-20"
       style={{ borderColor: COLORS.ink.medium }}
       onClick={handleClick}
     >
       <div className="flex items-start space-x-3">
         {/* 图片缩略图 */}
-        <img
-          src={news.images ? news.images.split(',')[0] : '/images/placeholder-banner.jpg'}
+        <ImageWithFallback
+          src={getThumbnailImage()}
           alt={news.newsTitle}
           className="w-16 h-16 rounded-md object-cover flex-shrink-0"
-          onError={(e) => {
-            (e.target as HTMLImageElement).src = '/images/placeholder-banner.jpg';
-          }}
+          fallbackSrc={PLACEHOLDER_IMAGE}
         />
         
         {/* 内容 */}

+ 8 - 8
src/client/mobile/data/mockPolicyNewsData.ts

@@ -8,7 +8,7 @@ export const mockPolicyNews: PolicyNewsItem[] = [
     newsContent: "国务院办公厅近日印发《关于积极应对人口老龄化的实施意见》,提出了一系列支持银龄群体的新政策。文件明确,到2025年,基本建成覆盖城乡、功能完善、规模适度、医养结合的养老服务体系。具体包括:提高基础养老金标准,完善长期护理保险制度,推进智慧养老服务平台建设,鼓励社会力量参与养老服务供给。同时,支持银龄人才再就业,建立灵活就业机制,为低龄健康老年人提供更多就业机会。",
     publishTime: new Date("2025-07-20T09:00:00Z"),
     viewCount: 1258,
-    images: "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=250&fit=crop",
+    images: "/images/banner1.jpg",
     summary: "2025年养老政策重大调整,涉及养老金、医疗保障、社区服务等多个方面",
     source: "中国政府网",
     category: "政策法规",
@@ -23,7 +23,7 @@ export const mockPolicyNews: PolicyNewsItem[] = [
     newsContent: "为充分发挥银龄人才资源优势,国家拟制定《银龄就业促进法》。草案明确禁止年龄歧视,要求用人单位不得设置不合理的年龄限制。建立银龄人才信息库,推动人才与企业精准对接。鼓励企业设立银龄岗位,给予税收优惠。完善银龄劳动者权益保护,包括工伤保险、职业培训等配套措施。",
     publishTime: new Date("2025-07-18T14:30:00Z"),
     viewCount: 892,
-    images: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400&h=250&fit=crop",
+    images: "/images/banner2.jpg",
     summary: "银龄就业促进法草案公开征求意见,重点关注反年龄歧视和就业保障",
     source: "人社部官网",
     category: "就业政策",
@@ -38,7 +38,7 @@ export const mockPolicyNews: PolicyNewsItem[] = [
     newsContent: "住建部发布《社区养老服务设施规划建设标准》,对社区养老服务设施的规划、建设、运营提出明确要求。新标准规定:新建小区按每万人不少于200平方米配建养老服务设施;老旧小区通过改造补充设施缺口;设施应包括日间照料、助餐服务、康复护理、文化娱乐等功能;鼓励采用智能化设备提升服务质量;建立设施运营评估机制,确保服务质量达标。",
     publishTime: new Date("2025-07-15T10:15:00Z"),
     viewCount: 567,
-    images: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400&h=250&fit=crop",
+    images: "/images/placeholder.jpg",
     summary: "社区养老服务设施建设有了新标准,要求每万人不少于200平方米",
     source: "住建部官网",
     category: "社区建设",
@@ -53,7 +53,7 @@ export const mockPolicyNews: PolicyNewsItem[] = [
     newsContent: "工信部联合多部门印发《智慧养老产业发展三年行动计划(2025-2027年)》,提出到2027年,智慧养老产业规模突破5万亿元。重点任务包括:开发适老化智能产品,建立智慧养老服务平台,推进AI、物联网、5G等技术在养老领域的应用;建设智慧养老示范区;完善智慧养老标准体系;支持企业加大研发投入,对符合条件的智慧养老产品给予税收优惠。",
     publishTime: new Date("2025-07-12T08:30:00Z"),
     viewCount: 743,
-    images: "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=250&fit=crop",
+    images: "/images/placeholder-banner.jpg",
     summary: "智慧养老产业迎来政策红利期,重点支持AI、物联网等技术在养老领域应用",
     source: "工信部官网",
     category: "产业发展",
@@ -68,7 +68,7 @@ export const mockPolicyNews: PolicyNewsItem[] = [
     newsContent: "国家卫健委发布《银龄健康管理体系建设指导意见》,提出建立覆盖城乡的银龄健康管理体系。主要内容包括:为65岁以上老年人建立健康档案,提供年度免费体检;开展慢性病筛查和干预;建立家庭医生签约服务制度;推进医养结合机构建设;完善老年人紧急医疗救助网络;支持社会力量举办老年病医院、康复医院、护理院等医疗机构。",
     publishTime: new Date("2025-07-10T15:45:00Z"),
     viewCount: 456,
-    images: "https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=400&h=250&fit=crop",
+    images: "/images/placeholder.jpg",
     summary: "构建覆盖全国的银龄健康管理体系,实现健康档案、慢病管理、紧急救助一体化",
     source: "国家卫健委",
     category: "健康政策",
@@ -83,7 +83,7 @@ export const mockPolicyNews: PolicyNewsItem[] = [
     newsContent: "《老年教育促进条例》于2025年7月1日起正式施行,条例明确了老年教育的地位、政府职责、保障措施等内容。条例规定:县级以上人民政府应当加强老年教育工作统筹规划;鼓励普通高等学校、职业院校开设老年教育专业;支持社会力量举办老年教育机构;建立老年教育师资培训体系;将老年教育经费列入财政预算;推动优质老年教育资源向基层延伸。",
     publishTime: new Date("2025-07-01T09:00:00Z"),
     viewCount: 1203,
-    images: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=250&fit=crop",
+    images: "/images/banner1.jpg",
     summary: "老年教育促进条例正式施行,保障老年人受教育权利,推动终身学习体系建设",
     source: "全国人大网",
     category: "教育政策",
@@ -114,5 +114,5 @@ export const getPolicyNewsByDate = () => {
   return [...mockPolicyNews].sort((a, b) => new Date(b.publishTime).getTime() - new Date(a.publishTime).getTime());
 };
 
-// 统一的占位图片配置
-export const PLACEHOLDER_IMAGE = 'https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=400&h=250&fit=crop';
+// 统一的占位图片配置 - 使用本地图片
+export const PLACEHOLDER_IMAGE = '/images/placeholder-banner.jpg';

+ 23 - 10
src/client/mobile/pages/PolicyNewsPage.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useRef } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { useQueryClient } from '@tanstack/react-query';
 import { usePolicyNewsData, type PolicyNewsItem } from '../hooks/usePolicyNewsData';
@@ -51,6 +51,7 @@ const PolicyNewsPage: React.FC = () => {
   const [filterCategory, setFilterCategory] = useState('');
   const [showFilters, setShowFilters] = useState(false);
   const [isRefreshing, setIsRefreshing] = useState(false);
+  const [imageErrors, setImageErrors] = useState<Set<number>>(new Set());
   
   const {
     data: policyNewsData,
@@ -72,21 +73,27 @@ const PolicyNewsPage: React.FC = () => {
   // 下拉刷新
   const handleRefresh = async () => {
     setIsRefreshing(true);
+    // 清除图片错误状态
+    setImageErrors(new Set());
     await queryClient.invalidateQueries({ queryKey: ['policy-news'] });
     setIsRefreshing(false);
   };
 
   // 搜索和过滤逻辑
-  const filteredNews = policyNewsData?.data.filter(news => {
-    const matchesSearch = searchQuery === '' || 
-      news.newsTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
-      news.summary?.toLowerCase().includes(searchQuery.toLowerCase());
+  const filteredNews = React.useMemo(() => {
+    if (!policyNewsData?.data) return [];
     
-    const matchesCategory = filterCategory === '' || 
-      news.category === filterCategory;
-    
-    return matchesSearch && matchesCategory;
-  });
+    return policyNewsData.data.filter(news => {
+      const matchesSearch = searchQuery === '' || 
+        news.newsTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
+        news.summary?.toLowerCase().includes(searchQuery.toLowerCase());
+      
+      const matchesCategory = filterCategory === '' || 
+        news.category === filterCategory;
+      
+      return matchesSearch && matchesCategory;
+    });
+  }, [policyNewsData?.data, searchQuery, filterCategory]);
 
   // 处理搜索
   const handleSearch = (e: React.FormEvent) => {
@@ -96,6 +103,12 @@ const PolicyNewsPage: React.FC = () => {
   // 清除搜索
   const clearSearch = () => {
     setSearchQuery('');
+    setFilterCategory('');
+  };
+
+  // 处理图片加载错误
+  const handleImageError = (newsId: number) => {
+    setImageErrors(prev => new Set(prev).add(newsId));
   };
 
   if (isLoading) {