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

✨ feat(mobile): 实现银龄人才列表数据获取功能

- 创建useSilverTalentsData和useSilverTalentDetail hooks,使用react-query管理API请求
- 从模拟数据切换到真实API数据源,支持搜索、分页功能
- 添加数据加载状态和错误处理
- 优化数据映射逻辑,将API响应转换为页面展示格式

♻️ refactor(server): 优化用户跟踪和职位推荐逻辑

- 为多个CRUD路由添加userIdField配置,完善用户关联
- 修改职位推荐算法,优化技能匹配逻辑,同时搜索职位描述和详情
- 增强GenericCrudService,支持设置userId字段进行数据关联
yourname 7 месяцев назад
Родитель
Сommit
216d5f55ad

+ 72 - 0
src/client/mobile/hooks/useSilverTalentsData.ts

@@ -0,0 +1,72 @@
+import { useQuery } from '@tanstack/react-query';
+import { silverTalentsClient } from '@/client/api';
+import { type InferResponseType } from 'hono/client';
+
+// 获取银龄人才列表响应类型
+type SilverTalentsResponse = InferResponseType<typeof silverTalentsClient.$get, 200>;
+type SilverTalent = SilverTalentsResponse['data'][0];
+
+export interface UseSilverTalentsDataOptions {
+  search?: string;
+  page?: number;
+  pageSize?: number;
+}
+
+export const useSilverTalentsData = (options: UseSilverTalentsDataOptions = {}) => {
+  const { search = '', page = 1, pageSize = 20 } = options;
+
+  const query = useQuery({
+    queryKey: ['silver-talents', search, page, pageSize],
+    queryFn: async () => {
+      const response = await silverTalentsClient.$get({
+        query: {
+          keyword: search,
+          page,
+          pageSize,
+          filters: JSON.stringify({
+            certificationStatus: 2 // 只显示已认证的人才
+          })
+        }
+      });
+
+      if (!response.ok) {
+        throw new Error('获取银龄人才数据失败');
+      }
+
+      const data = await response.json();
+      return data;
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟缓存
+    refetchOnWindowFocus: false,
+  });
+
+  return {
+    talents: query.data?.data || [],
+    total: query.data?.pagination?.total || 0,
+    isLoading: query.isLoading,
+    isError: query.isError,
+    error: query.error,
+    refetch: query.refetch,
+  };
+};
+
+// 获取单个银龄人才详情
+export const useSilverTalentDetail = (id: number) => {
+  return useQuery({
+    queryKey: ['silver-talent', id],
+    queryFn: async () => {
+      const response = await silverTalentsClient[':id'].$get({
+        param: { id: id.toString() }
+      });
+
+      if (!response.ok) {
+        throw new Error('获取银龄人才详情失败');
+      }
+
+      const data = await response.json();
+      return data;
+    },
+    enabled: !!id,
+    staleTime: 5 * 60 * 1000,
+  });
+};

+ 71 - 156
src/client/mobile/pages/SilverTalentsPage.tsx

@@ -1,6 +1,6 @@
 import React, { useState } from 'react';
 import { useNavigate } from 'react-router-dom';
-import { useQuery } from '@tanstack/react-query';
+import { useSilverTalentsData } from '../hooks/useSilverTalentsData';
 import { MagnifyingGlassIcon, UserIcon } from '@heroicons/react/24/outline';
 import { HeartIcon } from '@heroicons/react/24/solid';
 
@@ -24,138 +24,17 @@ const COLORS = {
   }
 };
 
-// 6组模拟数据 - 银龄人才
-const mockTalents = [
-  {
-    id: 1,
-    realName: '张老先生',
-    nickname: '张老师',
-    organization: '退休中学教师',
-    age: 65,
-    gender: 'MALE',
-    avatarUrl: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=400&h=400&fit=crop&crop=face',
-    personalIntro: '退休中学语文教师,40年教学经验,擅长书法和传统文化教育',
-    personalSkills: '书法,国画,古典文学,诗词创作,毛笔字教学',
-    certificationStatus: 'CERTIFIED',
-    jobSeekingStatus: 'ACTIVELY_SEEKING',
-    knowledgeShareCount: 8,
-    knowledgeReadCount: 356,
-    knowledgeRankingScore: 92.5,
-    totalPoints: 1250,
-  },
-  {
-    id: 2,
-    realName: '李奶奶',
-    nickname: '李阿姨',
-    organization: '退休护士长',
-    age: 62,
-    gender: 'FEMALE',
-    avatarUrl: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=400&h=400&fit=crop&crop=face',
-    personalIntro: '退休护士长,35年临床护理经验,擅长老年护理和健康咨询',
-    personalSkills: '老年护理,健康咨询,营养指导,基础医疗,心理辅导',
-    certificationStatus: 'CERTIFIED',
-    jobSeekingStatus: 'OPEN_TO_OPPORTUNITIES',
-    knowledgeShareCount: 6,
-    knowledgeReadCount: 289,
-    knowledgeRankingScore: 88.3,
-    totalPoints: 980,
-  },
-  {
-    id: 3,
-    realName: '王大爷',
-    nickname: '王师傅',
-    organization: '退休工程师',
-    age: 68,
-    gender: 'MALE',
-    avatarUrl: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop&crop=face',
-    personalIntro: '退休高级工程师,42年机械制造经验,擅长技术指导和工艺改进',
-    personalSkills: '机械设计,工艺改进,技术指导,质量管理,项目管理',
-    certificationStatus: 'CERTIFIED',
-    jobSeekingStatus: 'ACTIVELY_SEEKING',
-    knowledgeShareCount: 12,
-    knowledgeReadCount: 445,
-    knowledgeRankingScore: 95.2,
-    totalPoints: 1560,
-  },
-  {
-    id: 4,
-    realName: '陈阿姨',
-    nickname: '陈老师',
-    organization: '退休音乐教师',
-    age: 60,
-    gender: 'FEMALE',
-    avatarUrl: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=400&fit=crop&crop=face',
-    personalIntro: '退休音乐教师,38年音乐教育经验,擅长钢琴教学和声乐指导',
-    personalSkills: '钢琴教学,声乐指导,音乐理论,合唱指挥,乐器调律',
-    certificationStatus: 'CERTIFIED',
-    jobSeekingStatus: 'OPEN_TO_OPPORTUNITIES',
-    knowledgeShareCount: 7,
-    knowledgeReadCount: 312,
-    knowledgeRankingScore: 89.7,
-    totalPoints: 1100,
-  },
-  {
-    id: 5,
-    realName: '刘爷爷',
-    nickname: '刘大厨',
-    organization: '退休特级厨师',
-    age: 66,
-    gender: 'MALE',
-    avatarUrl: 'https://images.unsplash.com/photo-1557862921-37829c790f19?w=400&h=400&fit=crop&crop=face',
-    personalIntro: '退休特级厨师,45年烹饪经验,擅长川菜和家常菜制作',
-    personalSkills: '川菜制作,家常菜,面点制作,营养搭配,厨艺培训',
-    certificationStatus: 'CERTIFIED',
-    jobSeekingStatus: 'ACTIVELY_SEEKING',
-    knowledgeShareCount: 9,
-    knowledgeReadCount: 378,
-    knowledgeRankingScore: 91.8,
-    totalPoints: 1340,
-  },
-  {
-    id: 6,
-    realName: '赵奶奶',
-    nickname: '赵老师',
-    organization: '退休园艺师',
-    age: 63,
-    gender: 'FEMALE',
-    avatarUrl: 'https://images.unsplash.com/photo-1594744803329-e58b31de8bf5?w=400&h=400&fit=crop&crop=face',
-    personalIntro: '退休园艺师,40年园艺经验,擅长花卉养护和园林设计',
-    personalSkills: '花卉养护,园林设计,盆景制作,植物医生,园艺培训',
-    certificationStatus: 'CERTIFIED',
-    jobSeekingStatus: 'OPEN_TO_OPPORTUNITIES',
-    knowledgeShareCount: 5,
-    knowledgeReadCount: 267,
-    knowledgeRankingScore: 87.4,
-    totalPoints: 1050,
-  }
-];
-
 const SilverTalentsPage: React.FC = () => {
   const navigate = useNavigate();
   const [searchQuery, setSearchQuery] = useState('');
-
-  // 使用模拟数据
-  const { data, isLoading } = useQuery({
-    queryKey: ['silver-talents', searchQuery],
-    queryFn: async () => {
-      await new Promise(resolve => setTimeout(resolve, 800));
-      
-      let filteredTalents = mockTalents;
-      if (searchQuery) {
-        filteredTalents = mockTalents.filter(talent => 
-          talent.realName.includes(searchQuery) || 
-          talent.personalSkills.includes(searchQuery) ||
-          talent.personalIntro.includes(searchQuery) ||
-          talent.organization.includes(searchQuery)
-        );
-      }
-      
-      return { data: filteredTalents };
-    }
+  
+  // 使用真实API数据
+  const { talents, total, isLoading, isError, error } = useSilverTalentsData({
+    search: searchQuery,
+    page: 1,
+    pageSize: 20
   });
 
-  const talents = data?.data || [];
-
   // 技能标签颜色映射
   const skillColors = [
     { bg: 'bg-green-50', text: 'text-green-800', border: 'border-green-200' },
@@ -166,6 +45,40 @@ const SilverTalentsPage: React.FC = () => {
     { bg: 'bg-indigo-50', text: 'text-indigo-800', border: 'border-indigo-200' },
   ];
 
+  if (isError) {
+    return (
+      <div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: COLORS.ink.light }}>
+        <div className="text-center">
+          <h3 className="font-serif text-lg font-semibold mb-2" style={{ color: COLORS.text.secondary }}>
+            加载失败
+          </h3>
+          <p className="font-sans text-sm" style={{ color: COLORS.text.light }}>
+            {error?.message || '获取银龄人才数据失败,请稍后重试'}
+          </p>
+        </div>
+      </div>
+    );
+  }
+
+  // 映射API数据到页面展示格式
+  const mappedTalents = talents.map((talent: any) => ({
+    id: talent.id,
+    realName: talent.realName,
+    nickname: talent.nickname || '',
+    organization: talent.organization || '',
+    age: talent.age,
+    gender: talent.gender === 1 ? 'MALE' : talent.gender === 2 ? 'FEMALE' : 'OTHER',
+    avatarUrl: talent.avatarUrl,
+    personalIntro: talent.personalIntro || '',
+    personalSkills: talent.personalSkills || '',
+    certificationStatus: talent.certificationStatus === 2 ? 'CERTIFIED' : 'PENDING',
+    jobSeekingStatus: talent.jobSeekingStatus === 1 ? 'ACTIVELY_SEEKING' : talent.jobSeekingStatus === 2 ? 'OPEN_TO_OPPORTUNITIES' : 'NOT_SEEKING',
+    knowledgeShareCount: talent.knowledgeShareCount || 0,
+    knowledgeReadCount: talent.knowledgeReadCount || 0,
+    knowledgeRankingScore: parseFloat(talent.knowledgeRankingScore || 0),
+    totalPoints: talent.totalPoints || 0,
+  }));
+
   return (
     <div className="min-h-screen" style={{ backgroundColor: COLORS.ink.light }}>
       {/* 头部导航 - 水墨风格 */}
@@ -226,7 +139,7 @@ const SilverTalentsPage: React.FC = () => {
             }}
           >
             <div className="font-serif text-xl font-bold" style={{ color: COLORS.accent.green }}>
-              {talents.length}
+              {total}
             </div>
             <div className="font-sans text-xs" style={{ color: COLORS.text.secondary }}>
               认证人才
@@ -240,7 +153,7 @@ const SilverTalentsPage: React.FC = () => {
             }}
           >
             <div className="font-serif text-xl font-bold" style={{ color: COLORS.accent.blue }}>
-              {talents.reduce((sum, t) => sum + t.totalPoints, 0)}
+              {mappedTalents.reduce((sum, t) => sum + t.totalPoints, 0)}
             </div>
             <div className="font-sans text-xs" style={{ color: COLORS.text.secondary }}>
               总积分
@@ -254,7 +167,7 @@ const SilverTalentsPage: React.FC = () => {
             }}
           >
             <div className="font-serif text-xl font-bold" style={{ color: COLORS.accent.red }}>
-              {talents.reduce((sum, t) => sum + t.knowledgeShareCount, 0)}
+              {mappedTalents.reduce((sum, t) => sum + t.knowledgeShareCount, 0)}
             </div>
             <div className="font-sans text-xs" style={{ color: COLORS.text.secondary }}>
               知识分享
@@ -297,9 +210,9 @@ const SilverTalentsPage: React.FC = () => {
               </div>
             ))}
           </div>
-        ) : talents.length > 0 ? (
+        ) : mappedTalents.length > 0 ? (
           <div className="space-y-4">
-            {talents.map((talent: any) => (
+            {mappedTalents.map((talent) => (
               <div
                 key={talent.id}
                 className="p-4 rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer backdrop-blur-sm"
@@ -366,28 +279,30 @@ const SilverTalentsPage: React.FC = () => {
                     </p>
 
                     {/* 技能标签 */}
-                    <div className="flex flex-wrap gap-1 mb-3">
-                      {talent.personalSkills?.split(',').slice(0, 3).map((skill: string, i: number) => (
-                        <span 
-                          key={i} 
-                          className={`px-2 py-1 text-xs rounded-full border ${skillColors[i % skillColors.length].bg} ${skillColors[i % skillColors.length].text} ${skillColors[i % skillColors.length].border}`}
-                        >
-                          {skill.trim()}
-                        </span>
-                      ))}
-                      {talent.personalSkills?.split(',').length > 3 && (
-                        <span 
-                          className="px-2 py-1 text-xs rounded-full border"
-                          style={{ 
-                            backgroundColor: COLORS.ink.light,
-                            color: COLORS.text.secondary,
-                            borderColor: COLORS.ink.medium
-                          }}
-                        >
-                          +{talent.personalSkills.split(',').length - 3}
-                        </span>
-                      )}
-                    </div>
+                    {talent.personalSkills && (
+                      <div className="flex flex-wrap gap-1 mb-3">
+                        {talent.personalSkills.split(',').slice(0, 3).map((skill: string, i: number) => (
+                          <span 
+                            key={i} 
+                            className={`px-2 py-1 text-xs rounded-full border ${skillColors[i % skillColors.length].bg} ${skillColors[i % skillColors.length].text} ${skillColors[i % skillColors.length].border}`}
+                          >
+                            {skill.trim()}
+                          </span>
+                        ))}
+                        {talent.personalSkills.split(',').length > 3 && (
+                          <span 
+                            className="px-2 py-1 text-xs rounded-full border"
+                            style={{ 
+                              backgroundColor: COLORS.ink.light,
+                              color: COLORS.text.secondary,
+                              borderColor: COLORS.ink.medium
+                            }}
+                          >
+                            +{talent.personalSkills.split(',').length - 3}
+                          </span>
+                        )}
+                      </div>
+                    )}
 
                     {/* 统计信息 */}
                     <div className="flex items-center justify-between text-xs">
@@ -453,7 +368,7 @@ const SilverTalentsPage: React.FC = () => {
               className="font-sans text-sm"
               style={{ color: COLORS.text.light }}
             >
-              尝试调整搜索条件或清除搜索关键词
+              {searchQuery ? '尝试调整搜索条件或清除搜索关键词' : '暂无认证的银龄人才,敬请期待'}
             </p>
           </div>
         )}

+ 2 - 1
src/server/api/silver-jobs/applications/index.ts

@@ -13,7 +13,8 @@ const applicationRoutes = createCrudRoutes({
   middleware: [authMiddleware],
   userTracking: {
     createdByField: 'createdBy',
-    updatedByField: 'updatedBy'
+    updatedByField: 'updatedBy',
+    userIdField: 'userId'
   }
 });
 

+ 2 - 1
src/server/api/silver-jobs/favorites/index.ts

@@ -13,7 +13,8 @@ const favoriteRoutes = createCrudRoutes({
   middleware: [authMiddleware],
   userTracking: {
     createdByField: 'createdBy',
-    updatedByField: 'updatedBy'
+    updatedByField: 'updatedBy',
+    userIdField: 'userId'
   }
 });
 

+ 2 - 1
src/server/api/silver-users/knowledges/index.ts

@@ -18,7 +18,8 @@ const knowledgeRoutes = createCrudRoutes({
   middleware: [authMiddleware],
   userTracking: {
     createdByField: 'createdBy',
-    updatedByField: 'updatedBy'
+    updatedByField: 'updatedBy',
+    userIdField: 'userId'
   }
 });
 

+ 2 - 1
src/server/api/silver-users/points/index.ts

@@ -11,7 +11,8 @@ const silverPointRoutes = createCrudRoutes({
   middleware: [authMiddleware],
   userTracking: {
     createdByField: 'createdBy',
-    updatedByField: 'updatedBy'
+    updatedByField: 'updatedBy',
+    userIdField: 'userId'
   }
 });
 

+ 2 - 1
src/server/api/silver-users/profiles/index.ts

@@ -12,7 +12,8 @@ const silverUserProfileRoutes = createCrudRoutes({
   middleware: [authMiddleware],
   userTracking: {
     createdByField: 'createdBy',
-    updatedByField: 'updatedBy'
+    updatedByField: 'updatedBy',
+    userIdField: 'userId'
   }
 });
 

+ 2 - 1
src/server/api/silver-users/time-banks/index.ts

@@ -13,7 +13,8 @@ const silverTimeBankRoutes = createCrudRoutes({
   middleware: [authMiddleware],
   userTracking: {
     createdByField: 'createdBy',
-    updatedByField: 'updatedBy'
+    updatedByField: 'updatedBy',
+    userIdField: 'userId'
   }
 });
 

+ 8 - 7
src/server/modules/home/home.service.ts

@@ -138,17 +138,18 @@ export class HomeService {
     if (userProfile?.personalSkills) {
       const skills = userProfile.personalSkills.split(',').map((s: string) => s.trim()).filter(s => s);
       if (skills.length > 0) {
-        const orConditions = skills.map(skill => ({
-          requirements: Like(`%${skill}%`)
-        }));
-        
         queryBuilder.andWhere(
           new Brackets(qb => {
-            orConditions.forEach((condition, index) => {
+            skills.forEach((skill, index) => {
+              const skillCondition = new Brackets(skillQb => {
+                skillQb.where('job.description LIKE :skill', { skill: `%${skill}%` })
+                      .orWhere('job.details LIKE :skill', { skill: `%${skill}%` });
+              });
+              
               if (index === 0) {
-                qb.where(condition);
+                qb.where(skillCondition);
               } else {
-                qb.orWhere(condition);
+                qb.orWhere(skillCondition);
               }
             });
           })

+ 12 - 1
src/server/utils/generic-crud.service.ts

@@ -133,8 +133,13 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
       return;
     }
 
-    const { createdByField = 'createdBy', updatedByField = 'updatedBy' } = this.userTrackingOptions;
+    const {
+      createdByField = 'createdBy',
+      updatedByField = 'updatedBy',
+      userIdField = 'userId'
+    } = this.userTrackingOptions;
 
+    // 设置创建人和更新人
     if (isCreate && createdByField) {
       data[createdByField] = userId;
     }
@@ -142,6 +147,11 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     if (updatedByField) {
       data[updatedByField] = userId;
     }
+
+    // 设置关联的用户ID(如userId字段)
+    if (isCreate && userIdField) {
+      data[userIdField] = userId;
+    }
   }
 
   /**
@@ -183,6 +193,7 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
 export interface UserTrackingOptions {
   createdByField?: string;
   updatedByField?: string;
+  userIdField?: string;
 }
 
 export type CrudOptions<