Просмотр исходного кода

♻️ refactor(data): 迁移银龄智库数据到真实API

- 删除模拟数据文件mockSilverWisdomData.ts
- 修改useSilverWisdomData钩子使用真实API获取数据
- 更新SilverWisdomItem接口以匹配API响应结构
- 实现数据转换逻辑,将API响应映射为前端所需格式
- 添加阅读时间计算和精选标记逻辑

✨ feat(ui): 优化银龄智库页面展示

- 更新分类系统使用categoryId替代名称进行映射
- 改进统计卡片显示专家作者数量
- 优化日期格式化显示方式
- 重构标签显示逻辑,支持从API获取的标签数据
- 改进作者信息展示,使用默认头像替代图片链接

🔧 chore(types): 完善类型定义和接口注释

- 添加SilverKnowledgeResponse和SilverKnowledgeItem类型定义
- 为wisdomCategories添加类型安全保障
- 完善钩子函数返回值类型定义
- 添加工具函数注释说明功能
yourname 7 месяцев назад
Родитель
Сommit
a4859edef7

+ 0 - 156
src/client/mobile/data/mockSilverWisdomData.ts

@@ -1,156 +0,0 @@
-// 银龄智库模拟数据
-export interface SilverWisdomItem {
-  id: number;
-  title: string;
-  content: string;
-  category: string;
-  author: {
-    name: string;
-    avatar: string;
-    title: string;
-    organization: string;
-  };
-  coverImage: string;
-  viewCount: number;
-  likeCount: number;
-  commentCount: number;
-  readTime: number; // 阅读时间(分钟)
-  tags: string[];
-  publishDate: string;
-  isFeatured: boolean;
-}
-
-// 6组银龄智库知识内容模拟数据
-export const mockSilverWisdomData: SilverWisdomItem[] = [
-  {
-    id: 1,
-    title: "中医养生智慧:老年人四季调养指南",
-    content: "春季养肝、夏季养心、秋季养肺、冬季养肾,顺应自然规律的养生法则。老年人应特别注意饮食调理、情志调节和适度运动。分享我40年中医临床总结的有效养生方法,包括穴位按摩、食疗方剂和日常保健技巧。",
-    category: "健康养生",
-    author: {
-      name: "张老中医",
-      avatar: "https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=400&h=400&fit=crop&crop=face",
-      title: "资深中医专家",
-      organization: "北京中医学院附属医院"
-    },
-    coverImage: "https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?w=800&h=400&fit=crop",
-    viewCount: 2345,
-    likeCount: 156,
-    commentCount: 23,
-    readTime: 8,
-    tags: ["中医养生", "四季调养", "老年人健康", "食疗"],
-    publishDate: "2024-07-20",
-    isFeatured: true
-  },
-  {
-    id: 2,
-    title: "退休教师分享:如何保持大脑活力的10个方法",
-    content: "作为从教40年的老教师,我总结了一套保持大脑活力的方法。包括每日阅读、写作练习、棋类游戏、学习新技能、社交活动等。这些简单易行的方法帮助我在70岁仍然思维敏捷,记忆力良好。",
-    category: "认知训练",
-    author: {
-      name: "李老师",
-      avatar: "https://images.unsplash.com/photo-1568602471122-7832951cc4c5?w=400&h=400&fit=crop&crop=face",
-      title: "退休特级教师",
-      organization: "北京市第一中学"
-    },
-    coverImage: "https://images.unsplash.com/photo-1488190211105-8b0e65b80b4e?w=800&h=400&fit=crop",
-    viewCount: 1876,
-    likeCount: 198,
-    commentCount: 31,
-    readTime: 6,
-    tags: ["大脑训练", "认知保健", "退休生活", "学习技巧"],
-    publishDate: "2024-07-19",
-    isFeatured: true
-  },
-  {
-    id: 3,
-    title: "园艺疗愈:退休生活的绿色疗法",
-    content: "退休后的园艺生活不仅是一种爱好,更是一种疗愈方式。通过种植花草、培育盆景,我找到了内心的平静和生活的意义。分享适合老年人的园艺项目、种植技巧和园艺对身心健康的益处。",
-    category: "生活艺术",
-    author: {
-      name: "王园艺师",
-      avatar: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=400&h=400&fit=crop&crop=face",
-      title: "资深园艺专家",
-      organization: "中国园艺学会"
-    },
-    coverImage: "https://images.unsplash.com/photo-1466692476868-aef1dfb1e735?w=800&h=400&fit=crop",
-    viewCount: 1543,
-    likeCount: 134,
-    commentCount: 18,
-    readTime: 7,
-    tags: ["园艺疗愈", "退休生活", "花卉种植", "盆景艺术"],
-    publishDate: "2024-07-18",
-    isFeatured: false
-  },
-  {
-    id: 4,
-    title: "金融投资智慧:退休资金的稳健配置策略",
-    content: "基于我30年银行从业经验,为退休朋友分享资金配置的智慧。如何在保证资金安全的前提下获得稳定收益,如何防范金融诈骗,以及适合老年人的理财产品选择和风险评估。",
-    category: "理财规划",
-    author: {
-      name: "赵金融",
-      avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop&crop=face",
-      title: "退休银行家",
-      organization: "中国工商银行"
-    },
-    coverImage: "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800&h=400&fit=crop",
-    viewCount: 2891,
-    likeCount: 245,
-    commentCount: 42,
-    readTime: 10,
-    tags: ["退休理财", "资金配置", "风险防范", "稳健投资"],
-    publishDate: "2024-07-17",
-    isFeatured: true
-  },
-  {
-    id: 5,
-    title: "传统书法的艺术传承:老年人书法学习指南",
-    content: "书法不仅是一门艺术,更是一种修身养性的方式。作为从事书法教学50年的老教师,我想分享书法对老年人身心健康的益处、学习书法的正确方法、如何选择合适的工具和如何坚持每日练习。",
-    category: "艺术修养",
-    author: {
-      name: "陈书法家",
-      avatar: "https://images.unsplash.com/photo-1557862921-37829c790f19?w=400&h=400&fit=crop&crop=face",
-      title: "著名书法家",
-      organization: "中国书法家协会"
-    },
-    coverImage: "https://images.unsplash.com/photo-1547891654-e66ed7ebb968?w=800&h=400&fit=crop",
-    viewCount: 3124,
-    likeCount: 289,
-    commentCount: 56,
-    readTime: 9,
-    tags: ["书法艺术", "传统文化", "老年学习", "修身养性"],
-    publishDate: "2024-07-16",
-    isFeatured: true
-  },
-  {
-    id: 6,
-    title: "数字时代:老年人如何安全使用智能手机",
-    content: "面对智能手机和移动互联网,很多老年朋友感到困惑。作为IT专家,我将分享如何安全、便捷地使用智能手机,包括常用app使用、网络安全注意事项、防诈骗技巧,以及适合老年人的智能工具推荐。",
-    category: "科技应用",
-    author: {
-      name: "刘工程师",
-      avatar: "https://images.unsplash.com/photo-1519345182560-3f2917c472ef?w=400&h=400&fit=crop&crop=face",
-      title: "退休IT专家",
-      organization: "华为技术研究院"
-    },
-    coverImage: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&h=400&fit=crop",
-    viewCount: 3657,
-    likeCount: 412,
-    commentCount: 78,
-    readTime: 12,
-    tags: ["智能手机", "网络安全", "数字生活", "科技应用"],
-    publishDate: "2024-07-15",
-    isFeatured: false
-  }
-];
-
-// 知识分类列表
-export const wisdomCategories = [
-  { value: 'all', label: '全部', icon: '📚' },
-  { value: '健康养生', label: '健康养生', icon: '🏥' },
-  { value: '认知训练', label: '认知训练', icon: '🧠' },
-  { value: '生活艺术', label: '生活艺术', icon: '🎨' },
-  { value: '理财规划', label: '理财规划', icon: '💰' },
-  { value: '艺术修养', label: '艺术修养', icon: '🎭' },
-  { value: '科技应用', label: '科技应用', icon: '📱' }
-];

+ 138 - 55
src/client/mobile/hooks/useSilverWisdomData.ts

@@ -1,66 +1,113 @@
 import { useState, useMemo } from 'react';
 import { useQuery } from '@tanstack/react-query';
-import { mockSilverWisdomData, SilverWisdomItem, wisdomCategories } from '../data/mockSilverWisdomData';
+import { silverKnowledgeClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
 
-export const useSilverWisdomData = (searchQuery?: string, category?: string) => {
-  const [selectedCategory, setSelectedCategory] = useState<string>(category || 'all');
+// 定义移动端银龄智库数据结构,基于实体定义
+export interface SilverWisdomItem {
+  id: number;
+  title: string;
+  content: string;
+  category: string;
+  categoryId: number;
+  author: string;
+  status: number;
+  viewCount: number;
+  likeCount: number;
+  coverImage: string | null;
+  tags: string | null;
+  attachment: string | null;
+  attachmentName: string | null;
+  createdAt: string;
+  updatedAt: string;
+  readTime?: number; // 计算字段
+  commentCount?: number; // 可选字段
+  isFeatured?: boolean; // 可选字段
+}
 
-  // 使用模拟数据的查询
-  const { data, isLoading, isError, error } = useQuery({
-    queryKey: ['silver-wisdom', searchQuery, selectedCategory],
-    queryFn: async () => {
-      // 模拟API延迟
-      await new Promise(resolve => setTimeout(resolve, 800));
-      
-      let filteredData = mockSilverWisdomData;
+// 定义分类
+export const wisdomCategories = [
+  { value: 'all', label: '全部', icon: '📚' },
+  { value: 1, label: '健康养生', icon: '🏥' },
+  { value: 2, label: '认知训练', icon: '🧠' },
+  { value: 3, label: '生活艺术', icon: '🎨' },
+  { value: 4, label: '理财规划', icon: '💰' },
+  { value: 5, label: '艺术修养', icon: '🎭' },
+  { value: 6, label: '科技应用', icon: '📱' }
+];
 
-      // 按分类筛选
-      if (selectedCategory && selectedCategory !== 'all') {
-        filteredData = filteredData.filter(item => item.category === selectedCategory);
-      }
+// 从API响应提取类型
+type SilverKnowledgeResponse = InferResponseType<typeof silverKnowledgeClient.$get, 200>;
+type SilverKnowledgeItem = SilverKnowledgeResponse['data'][0];
 
-      // 按搜索关键词筛选
-      if (searchQuery?.trim()) {
-        const query = searchQuery.toLowerCase();
-        filteredData = filteredData.filter(item => 
-          item.title.toLowerCase().includes(query) ||
-          item.content.toLowerCase().includes(query) ||
-          item.author.name.toLowerCase().includes(query) ||
-          item.tags.some(tag => tag.toLowerCase().includes(query)) ||
-          item.category.toLowerCase().includes(query)
-        );
-      }
+export const useSilverWisdomData = (searchQuery?: string, categoryId?: number) => {
+  const [selectedCategory, setSelectedCategory] = useState<number | string>(categoryId || 'all');
 
-      // 按精选和发布时间排序
-      return filteredData.sort((a, b) => {
-        if (a.isFeatured !== b.isFeatured) {
-          return a.isFeatured ? -1 : 1;
+  // 使用真实API数据
+  const { data, isLoading, isError, error } = useQuery({
+    queryKey: ['silver-wisdom', searchQuery, selectedCategory],
+    queryFn: async () => {
+      const response = await silverKnowledgeClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          keyword: searchQuery || undefined,
+          ...(selectedCategory !== 'all' && { filters: JSON.stringify({ categoryId: selectedCategory }) })
         }
-        return new Date(b.publishDate).getTime() - new Date(a.publishDate).getTime();
       });
+      
+      if (!response.ok) {
+        throw new Error('获取数据失败');
+      }
+      
+      const result = await response.json();
+      return result.data;
     },
-    staleTime: 5 * 60 * 1000, // 5分钟
-    gcTime: 10 * 60 * 1000,  // 10分钟
+    staleTime: 5 * 60 * 1000,
+    gcTime: 10 * 60 * 1000,
   });
 
+  // 转换API数据为移动端格式
+  const wisdomItems: SilverWisdomItem[] = useMemo(() => {
+    if (!data) return [];
+    
+    return data.map((item: SilverKnowledgeItem) => ({
+      id: item.id,
+      title: item.title,
+      content: item.content,
+      category: item.category?.name || '未分类',
+      categoryId: item.categoryId,
+      author: item.author,
+      status: item.status,
+      viewCount: item.viewCount,
+      likeCount: item.likeCount,
+      coverImage: item.coverImage,
+      tags: item.tags,
+      attachment: item.attachment,
+      attachmentName: item.attachmentName,
+      createdAt: item.createdAt,
+      updatedAt: item.updatedAt,
+      // 计算阅读时间(按每分钟200字估算)
+      readTime: Math.max(1, Math.ceil(item.content.length / 200)),
+      // 评论数暂用0,后续可扩展
+      commentCount: 0,
+      // 精选规则:浏览量>1000且点赞率>5%
+      isFeatured: item.viewCount > 1000 && (item.likeCount / item.viewCount) > 0.05
+    }));
+  }, [data]);
+
   // 统计信息
   const stats = useMemo(() => {
-    const items = data || mockSilverWisdomData;
     return {
-      totalCount: items.length,
-      categoryCounts: wisdomCategories.reduce((acc, cat) => {
-        if (cat.value !== 'all') {
-          acc[cat.value] = items.filter(item => item.category === cat.value).length;
-        }
-        return acc;
-      }, {} as Record<string, number>),
-      totalViews: items.reduce((sum, item) => sum + item.viewCount, 0),
-      totalLikes: items.reduce((sum, item) => sum + item.likeCount, 0)
+      totalCount: wisdomItems.length,
+      totalViews: wisdomItems.reduce((sum, item) => sum + item.viewCount, 0),
+      totalLikes: wisdomItems.reduce((sum, item) => sum + item.likeCount, 0),
+      expertCount: new Set(wisdomItems.map(item => item.author)).size
     };
-  }, [data]);
+  }, [wisdomItems]);
 
   return {
-    wisdomItems: data || [],
+    wisdomItems,
     categories: wisdomCategories,
     selectedCategory,
     setSelectedCategory,
@@ -76,12 +123,16 @@ export const useSilverWisdomDetail = (id: number) => {
   return useQuery({
     queryKey: ['silver-wisdom-detail', id],
     queryFn: async () => {
-      await new Promise(resolve => setTimeout(resolve, 500));
-      const item = mockSilverWisdomData.find(item => item.id === id);
-      if (!item) {
-        throw new Error('知识内容不存在');
+      const response = await silverKnowledgeClient[':id'].$get({
+        param: { id: id.toString() }
+      });
+      
+      if (!response.ok) {
+        throw new Error('获取详情失败');
       }
-      return item;
+      
+      const result = await response.json();
+      return result;
     }
   });
 };
@@ -91,16 +142,48 @@ export const useRecommendedWisdom = (excludeId?: number) => {
   return useQuery({
     queryKey: ['recommended-wisdom', excludeId],
     queryFn: async () => {
-      await new Promise(resolve => setTimeout(resolve, 500));
-      let items = [...mockSilverWisdomData];
+      const response = await silverKnowledgeClient.$get({
+        query: {
+          page: 1,
+          pageSize: 10,
+          ...(excludeId && { filters: JSON.stringify({ id: { gt: 0 } }) })
+        }
+      });
+      
+      if (!response.ok) {
+        throw new Error('获取推荐失败');
+      }
+      
+      const result = await response.json();
+      let items = result.data || [];
       
       if (excludeId) {
-        items = items.filter(item => item.id !== excludeId);
+        items = items.filter((item: SilverKnowledgeItem) => item.id !== excludeId);
       }
       
       return items
-        .sort((a, b) => b.viewCount - a.viewCount)
-        .slice(0, 4);
+        .sort((a: SilverKnowledgeItem, b: SilverKnowledgeItem) => b.viewCount - a.viewCount)
+        .slice(0, 4)
+        .map((item: SilverKnowledgeItem) => ({
+          id: item.id,
+          title: item.title,
+          content: item.content,
+          category: item.category?.name || '未分类',
+          categoryId: item.categoryId,
+          author: item.author,
+          status: item.status,
+          viewCount: item.viewCount,
+          likeCount: item.likeCount,
+          coverImage: item.coverImage,
+          tags: item.tags,
+          attachment: item.attachment,
+          attachmentName: item.attachmentName,
+          createdAt: item.createdAt,
+          updatedAt: item.updatedAt,
+          readTime: Math.max(1, Math.ceil(item.content.length / 200)),
+          commentCount: 0,
+          isFeatured: item.viewCount > 1000 && (item.likeCount / item.viewCount) > 0.05
+        }));
     }
   });
 };

+ 54 - 47
src/client/mobile/pages/SilverWisdomPage.tsx

@@ -1,7 +1,7 @@
 import React, { useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { useSilverWisdomData } from '../hooks/useSilverWisdomData';
-import { SilverWisdomItem } from '../data/mockSilverWisdomData';
+import type { SilverWisdomItem } from '../hooks/useSilverWisdomData';
 import {
   MagnifyingGlassIcon,
   UserIcon,
@@ -9,10 +9,8 @@ import {
   HeartIcon,
   ChatBubbleLeftIcon,
   ClockIcon,
-  TagIcon,
   BookOpenIcon
 } from '@heroicons/react/24/outline';
-import { HeartIcon as HeartSolidIcon } from '@heroicons/react/24/solid';
 
 // 中国水墨风格色彩系统
 const COLORS = {
@@ -43,14 +41,24 @@ const FONT_STYLES = {
   small: 'font-sans text-xs',
 };
 
-// 分类标签颜色映射
-const categoryColors: Record<string, string> = {
-  '健康养生': 'bg-green-50 text-green-800 border-green-200',
-  '认知训练': 'bg-blue-50 text-blue-800 border-blue-200',
-  '生活艺术': 'bg-purple-50 text-purple-800 border-purple-200',
-  '理财规划': 'bg-orange-50 text-orange-800 border-orange-200',
-  '艺术修养': 'bg-pink-50 text-pink-800 border-pink-200',
-  '科技应用': 'bg-indigo-50 text-indigo-800 border-indigo-200',
+// 分类标签颜色映射 - 基于categoryId映射
+const categoryColors: Record<number, string> = {
+  1: 'bg-green-50 text-green-800 border-green-200', // 健康养生
+  2: 'bg-blue-50 text-blue-800 border-blue-200',   // 认知训练
+  3: 'bg-purple-50 text-purple-800 border-purple-200', // 生活艺术
+  4: 'bg-orange-50 text-orange-800 border-orange-200', // 理财规划
+  5: 'bg-pink-50 text-pink-800 border-pink-200',   // 艺术修养
+  6: 'bg-indigo-50 text-indigo-800 border-indigo-200', // 科技应用
+};
+
+// 分类名称映射
+const categoryNames: Record<number, string> = {
+  1: '健康养生',
+  2: '认知训练',
+  3: '生活艺术',
+  4: '理财规划',
+  5: '艺术修养',
+  6: '科技应用',
 };
 
 // 工具函数:格式化数字
@@ -272,7 +280,7 @@ const SilverWisdomPage: React.FC = () => {
           >
             <UserIcon className="w-6 h-6 mx-auto mb-1" style={{ color: COLORS.ink.dark }} />
             <div className="font-serif text-lg font-bold" style={{ color: COLORS.ink.dark }}>
-              6
+              {formatNumber(stats.expertCount)}
             </div>
             <div className={`${FONT_STYLES.small}`} style={{ color: COLORS.text.secondary }}>
               专家作者
@@ -390,13 +398,13 @@ const WisdomCard: React.FC<{
       <div className="p-4">
         {/* 分类标签 */}
         <div className="flex items-center justify-between mb-2">
-          <span 
-            className={`px-2 py-1 rounded-full text-xs ${categoryColors[item.category] || 'bg-gray-100 text-gray-800 border-gray-200'}`}
+          <span
+            className={`px-2 py-1 rounded-full text-xs ${categoryColors[item.categoryId] || 'bg-gray-100 text-gray-800 border-gray-200'}`}
           >
-            {item.category}
+            {categoryNames[item.categoryId] || item.category}
           </span>
           <span className={`${FONT_STYLES.small}`} style={{ color: COLORS.text.light }}>
-            {item.publishDate}
+            {formatDate(item.createdAt)}
           </span>
         </div>
 
@@ -418,45 +426,44 @@ const WisdomCard: React.FC<{
 
         {/* 作者信息 */}
         <div className="flex items-center mb-3">
-          <img
-            src={item.author.avatar}
-            alt={item.author.name}
-            className="w-8 h-8 rounded-full border-2 mr-2"
-            style={{ borderColor: COLORS.ink.medium }}
-          />
+          <div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center mr-2">
+            <UserIcon className="w-4 h-4" style={{ color: COLORS.ink.dark }} />
+          </div>
           <div>
             <p className={`${FONT_STYLES.small} font-medium`} style={{ color: COLORS.text.primary }}>
-              {item.author.name}
+              {item.author}
             </p>
             <p className={`${FONT_STYLES.small}`} style={{ color: COLORS.text.light }}>
-              {item.author.title}
+              知识分享者
             </p>
           </div>
         </div>
 
         {/* 标签 */}
-        <div className="flex flex-wrap gap-1 mb-3">
-          {item.tags.slice(0, 3).map((tag, index) => (
-            <span 
-              key={index}
-              className={`px-2 py-1 rounded-full text-xs ${categoryColors[item.category] || 'bg-gray-100 text-gray-800 border-gray-200'}`}
-            >
-              {tag}
-            </span>
-          ))}
-          {item.tags.length > 3 && (
-            <span 
-              className={`px-2 py-1 rounded-full text-xs`}
-              style={{ 
-                backgroundColor: COLORS.ink.light,
-                color: COLORS.text.secondary,
-                border: `1px solid ${COLORS.ink.medium}`
-              }}
-            >
-              +{item.tags.length - 3}
-            </span>
-          )}
-        </div>
+        {item.tags && (
+          <div className="flex flex-wrap gap-1 mb-3">
+            {item.tags.split(',').slice(0, 3).map((tag, index) => (
+              <span
+                key={index}
+                className={`px-2 py-1 rounded-full text-xs ${categoryColors[item.categoryId] || 'bg-gray-100 text-gray-800 border-gray-200'}`}
+              >
+                {tag.trim()}
+              </span>
+            ))}
+            {item.tags.split(',').length > 3 && (
+              <span
+                className={`px-2 py-1 rounded-full text-xs`}
+                style={{
+                  backgroundColor: COLORS.ink.light,
+                  color: COLORS.text.secondary,
+                  border: `1px solid ${COLORS.ink.medium}`
+                }}
+              >
+                +{item.tags.split(',').length - 3}
+              </span>
+            )}
+          </div>
+        )}
 
         {/* 统计信息 */}
         <div className="flex items-center justify-between text-xs">
@@ -476,7 +483,7 @@ const WisdomCard: React.FC<{
           </div>
           <span className="flex items-center" style={{ color: COLORS.text.light }}>
             <ClockIcon className="w-4 h-4 mr-1" />
-            {formatDate(item.publishDate)}
+            {formatDate(item.createdAt)}
           </span>
         </div>
       </div>