Pārlūkot izejas kodu

✨ feat(mobile): 添加人才详情调试页面并优化详情页UI

- 新增SilverTalentDebugPage页面用于验证API响应格式
- 优化TalentDetailPage页面UI,采用水墨风格设计
- 添加错误处理和加载状态显示
- 增加个人简介、专业技能、工作经历等信息展示区域
- 实现响应式布局和交互效果
- 添加底部操作按钮区域,支持联系、收藏和分享功能

🐛 fix(mobile): 修复人才详情页数据获取问题

- 添加API请求错误处理逻辑
- 兼容不同格式的API响应数据
- 修复ID参数验证问题,确保只在有效ID时发起请求
- 添加请求重试机制,提高稳定性

💄 style(mobile): 优化人才详情页视觉设计

- 定义水墨风格色彩系统
- 添加卡片悬停效果和过渡动画
- 优化排版层次和字体样式
- 统一按钮和交互元素样式
- 实现骨架屏加载效果,提升用户体验
yourname 7 mēneši atpakaļ
vecāks
revīzija
7dbb6ab92a

+ 105 - 0
src/client/mobile/pages/SilverTalentDebugPage.tsx

@@ -0,0 +1,105 @@
+import React from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { ArrowLeftIcon } from '@heroicons/react/24/outline';
+import { useQuery } from '@tanstack/react-query';
+import { silverTalentsClient } from '@/client/api';
+
+// 调试页面,用于验证API响应格式
+const SilverTalentDebugPage: React.FC = () => {
+  const { id } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+
+  const { data, isLoading, error } = useQuery({
+    queryKey: ['talent-debug', id],
+    queryFn: async () => {
+      console.log('🚀 开始获取人才详情,ID:', id);
+      
+      if (!id || isNaN(Number(id))) {
+        throw new Error('❌ 无效的ID参数');
+      }
+      
+      const response = await silverTalentsClient[':id'].$get({
+        param: { id: Number(id) }
+      });
+      
+      console.log('📡 响应状态:', response.status);
+      
+      if (!response.ok) {
+        throw new Error(`❌ API请求失败: ${response.status}`);
+      }
+      
+      const result = await response.json();
+      console.log('📋 API响应数据:', result);
+      return result;
+    },
+    retry: 1,
+    enabled: !!id && !isNaN(Number(id)),
+  });
+
+  if (isLoading) {
+    return (
+      <div className="p-4">
+        <div className="text-center">
+          <h2 className="text-lg font-bold mb-2">正在加载...</h2>
+          <p>正在获取人才信息,请稍候</p>
+        </div>
+      </div>
+    );
+  }
+
+  if (error) {
+    return (
+      <div className="p-4">
+        <div className="text-center">
+          <h2 className="text-lg font-bold mb-2 text-red-600">加载失败</h2>
+          <p className="text-gray-600 mb-4">{error.message}</p>
+          <button 
+            onClick={() => navigate(-1)}
+            className="px-4 py-2 bg-blue-500 text-white rounded"
+          >
+            返回
+          </button>
+        </div>
+      </div>
+    );
+  }
+
+  // 调试显示原始数据
+  const talentData = data?.data || data;
+
+  return (
+    <div className="p-4">
+      <div className="flex items-center mb-4">
+        <button 
+          onClick={() => navigate(-1)}
+          className="mr-2"
+        >
+          <ArrowLeftIcon className="w-5 h-5" />
+        </button>
+        <h1 className="text-lg font-bold">人才详情调试</h1>
+      </div>
+
+      <div className="bg-gray-100 p-4 rounded mb-4">
+        <h3 className="font-bold mb-2">调试信息</h3>
+        <p><strong>人才ID:</strong> {id}</p>
+        <p><strong>数据存在:</strong> {talentData ? '✅' : '❌'}</p>
+        <p><strong>响应格式:</strong> {data ? (data.data ? '{ data: ... }' : '直接数据') : '无数据'}</p>
+      </div>
+
+      {talentData ? (
+        <div className="bg-white p-4 rounded shadow">
+          <h3 className="font-bold mb-2">人才数据</h3>
+          <pre className="text-xs overflow-auto bg-gray-50 p-2 rounded">
+            {JSON.stringify(talentData, null, 2)}
+          </pre>
+        </div>
+      ) : (
+        <div className="text-center text-gray-500">
+          <p>暂无人才数据</p>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default SilverTalentDebugPage;

+ 314 - 70
src/client/mobile/pages/TalentDetailPage.tsx

@@ -1,118 +1,362 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { useParams, useNavigate } from 'react-router-dom';
-import { ArrowLeftIcon, MapPinIcon, StarIcon } from '@heroicons/react/24/outline';
+import { ArrowLeftIcon, PhoneIcon, EnvelopeIcon, StarIcon, CheckCircleIcon, ClockIcon, BriefcaseIcon, UserGroupIcon } from '@heroicons/react/24/outline';
 import { useQuery } from '@tanstack/react-query';
 import { silverTalentsClient } from '@/client/api';
 
+// 水墨风格色彩常量
+const COLORS = {
+  ink: {
+    light: '#f5f3f0',
+    medium: '#d4c4a8',
+    dark: '#8b7355',
+    deep: '#3a2f26',
+  },
+  accent: {
+    red: '#a85c5c',
+    blue: '#4a6b7c',
+    green: '#5c7c5c',
+  },
+  text: {
+    primary: '#2f1f0f',
+    secondary: '#5d4e3b',
+    light: '#8b7355',
+  }
+};
+
 const TalentDetailPage: React.FC = () => {
   const { id } = useParams<{ id: string }>();
   const navigate = useNavigate();
+  const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
 
-  const { data, isLoading } = useQuery({
+  const { data, isLoading, error } = useQuery({
     queryKey: ['talent-detail', id],
     queryFn: async () => {
       const response = await silverTalentsClient[':id'].$get({
         param: { id: Number(id) }
       });
-      return response.json();
-    }
+      if (!response.ok) {
+        throw new Error('获取人才信息失败');
+      }
+      const result = await response.json();
+      return result;
+    },
+    retry: 1,
+    enabled: !!id && !isNaN(Number(id)),
   });
 
-  const talent = data?.data;
+  // 兼容不同的API响应格式
+  const talent = data?.data || data;
+
+  const toggleSection = (section: string) => {
+    setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }));
+  };
+
+  const formatGender = (gender: number) => {
+    const genderMap: Record<number, string> = {
+      1: '男',
+      2: '女',
+      3: '其他'
+    };
+    return genderMap[gender] || '未知';
+  };
+
+  const formatCertificationStatus = (status: number) => {
+    const statusMap: Record<number, { text: string; color: string }> = {
+      0: { text: '未认证', color: COLORS.text.light },
+      1: { text: '认证中', color: COLORS.accent.blue },
+      2: { text: '已认证', color: COLORS.accent.green },
+      3: { text: '已拒绝', color: COLORS.accent.red }
+    };
+    return statusMap[status] || { text: '未知', color: COLORS.text.light };
+  };
+
+  const formatJobSeekingStatus = (status: number) => {
+    const statusMap: Record<number, string> = {
+      0: '未求职',
+      1: '积极求职',
+      2: '观望机会'
+    };
+    return statusMap[status] || '未知';
+  };
 
   if (isLoading) {
     return (
-      <div className="min-h-screen bg-gray-50 p-4">
-        <div className="bg-white rounded-lg p-4">
-          <div className="h-32 bg-gray-200 rounded-lg mb-4"></div>
-          <div className="space-y-3">
-            <div className="h-6 bg-gray-200 rounded w-1/4"></div>
-            <div className="h-4 bg-gray-200 rounded w-3/4"></div>
-            <div className="h-4 bg-gray-200 rounded w-1/2"></div>
-          </div>
+      <div className="min-h-screen" style={{ backgroundColor: COLORS.ink.light }}>
+        {/* 骨架屏加载效果 */}
+        <header className="sticky top-0 z-10 border-b border-opacity-20 px-4 py-4"
+          style={{ backgroundColor: COLORS.ink.light, borderColor: COLORS.ink.medium }}>
+          <div className="h-6 bg-gray-200 rounded w-20 animate-pulse"></div>
+        </header>
+        
+        <div className="p-4 space-y-4">
+          {[1, 2, 3, 4, 5].map(i => (
+            <div key={i} className="rounded-xl p-4 shadow-sm backdrop-blur-sm animate-pulse"
+              style={{ backgroundColor: 'rgba(255,255,255,0.8)', border: `1px solid ${COLORS.ink.medium}` }}>
+              <div className="h-6 bg-gray-200 rounded w-1/3 mb-3"></div>
+              <div className="space-y-2">
+                <div className="h-4 bg-gray-200 rounded w-full"></div>
+                <div className="h-4 bg-gray-200 rounded w-3/4"></div>
+                <div className="h-4 bg-gray-200 rounded w-1/2"></div>
+              </div>
+            </div>
+          ))}
+        </div>
+      </div>
+    );
+  }
+
+  if (error || !talent) {
+    return (
+      <div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: COLORS.ink.light }}>
+        <div className="text-center">
+          <div className="text-6xl mb-4">😔</div>
+          <h3 className="text-xl font-serif" style={{ color: COLORS.text.primary }}>获取人才信息失败</h3>
+          <p className="text-sm mt-2" style={{ color: COLORS.text.secondary }}>请检查网络连接后重试</p>
+          <button
+            onClick={() => navigate(-1)}
+            className="mt-4 px-4 py-2 rounded-full text-sm transition-all duration-300 hover:shadow-md"
+            style={{ backgroundColor: COLORS.ink.dark, color: 'white' }}
+          >
+            返回上一页
+          </button>
         </div>
       </div>
     );
   }
 
   return (
-    <div className="min-h-screen bg-gray-50">
-      {/* 头部导航 */}
-      <header className="sticky top-0 bg-white shadow-sm px-4 py-4">
-        <button onClick={() => navigate(-1)} className="flex items-center text-gray-600">
-          <ArrowLeftIcon className="w-5 h-5 mr-2" />
-          返回
-        </button>
+    <div className="min-h-screen" style={{ backgroundColor: COLORS.ink.light }}>
+      {/* 顶部导航栏 */}
+      <header className="shadow-sm sticky top-0 z-10 border-b border-opacity-20 px-4 py-3"
+        style={{ backgroundColor: COLORS.ink.light, borderColor: COLORS.ink.medium }}>
+        <div className="flex items-center">
+          <button
+            onClick={() => navigate(-1)}
+            className="flex items-center transition-colors duration-200"
+            style={{ color: COLORS.text.primary }}
+          >
+            <ArrowLeftIcon className="w-5 h-5" />
+            <span className="ml-2 text-sm font-medium">返回</span>
+          </button>
+        </div>
       </header>
 
-      {talent && (
-        <div className="p-4 space-y-4">
-          {/* 个人信息卡片 */}
-          <div className="bg-white rounded-lg p-4">
-            <div className="flex items-center space-x-4">
-              <img
-                src={talent.avatarUrl || '/images/avatar-placeholder.jpg'}
-                alt={talent.realName}
-                className="w-20 h-20 rounded-full object-cover"
-              />
-              <div>
-                <h1 className="text-xl font-bold text-gray-800">{talent.realName}</h1>
-                <p className="text-gray-600">
-                  {talent.age}岁 · {talent.nickname || talent.realName}
+      <div className="p-4 space-y-4 pb-20">
+        {/* 用户基本信息卡片 */}
+        <div className="rounded-xl p-4 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur-sm"
+          style={{ backgroundColor: 'rgba(255,255,255,0.8)', border: `1px solid ${COLORS.ink.medium}` }}>
+          <div className="flex items-start space-x-4">
+            <img
+              src={talent.avatarUrl || '/images/avatar-placeholder.jpg'}
+              alt={talent.realName}
+              className="w-20 h-20 rounded-full object-cover border-2 flex-shrink-0"
+              style={{ borderColor: COLORS.ink.medium }}
+            />
+            <div className="flex-1 min-w-0">
+              <div className="flex items-center space-x-2 mb-1">
+                <h1 className="font-serif text-xl font-bold tracking-wide truncate"
+                  style={{ color: COLORS.text.primary }}>
+                  {talent.realName}
+                </h1>
+                <span className="px-2 py-0.5 rounded-full text-xs font-medium flex-shrink-0"
+                  style={{
+                    backgroundColor: formatCertificationStatus(talent.certificationStatus).color,
+                    color: 'white'
+                  }}>
+                  {formatCertificationStatus(talent.certificationStatus).text}
+                </span>
+              </div>
+              
+              <p className="text-sm mb-1" style={{ color: COLORS.text.secondary }}>
+                {talent.age}岁 · {formatGender(talent.gender)}
+                {talent.nickname && ` · ${talent.nickname}`}
+              </p>
+              
+              {talent.organization && (
+                <p className="text-sm mb-2" style={{ color: COLORS.text.secondary }}>
+                  {talent.organization}
                 </p>
-                <p className="text-sm text-gray-500">{talent.organization}</p>
-                <div className="flex items-center mt-2">
-                  <MapPinIcon className="w-4 h-4 mr-1 text-gray-400" />
-                  <span className="text-sm text-gray-600">中国</span>
-                </div>
+              )}
+              
+              <div className="flex items-center space-x-4 text-xs">
+                {talent.phone && (
+                  <a href={`tel:${talent.phone}`} className="flex items-center space-x-1">
+                    <PhoneIcon className="w-3 h-3" style={{ color: COLORS.accent.blue }} />
+                    <span style={{ color: COLORS.accent.blue }}>{talent.phone}</span>
+                  </a>
+                )}
+                {talent.email && (
+                  <a href={`mailto:${talent.email}`} className="flex items-center space-x-1">
+                    <EnvelopeIcon className="w-3 h-3" style={{ color: COLORS.accent.blue }} />
+                    <span style={{ color: COLORS.accent.blue }}>邮箱</span>
+                  </a>
+                )}
               </div>
             </div>
           </div>
+        </div>
+
+        {/* 个人简介卡片 */}
+        {talent.personalIntro && (
+          <div className="rounded-xl p-4 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur-sm"
+            style={{ backgroundColor: 'rgba(255,255,255,0.8)', border: `1px solid ${COLORS.ink.medium}` }}>
+            <h2 className="font-serif text-lg font-semibold tracking-wide mb-3"
+              style={{ color: COLORS.text.primary }}>
+              个人简介
+            </h2>
+            <p className="text-sm leading-relaxed" style={{ color: COLORS.text.secondary }}>
+              {talent.personalIntro}
+            </p>
+          </div>
+        )}
 
-          {/* 技能标签 */}
-          <div className="bg-white rounded-lg p-4">
-            <h2 className="font-bold text-gray-800 mb-2">专业技能</h2>
+        {/* 专业技能卡片 */}
+        {talent.personalSkills && (
+          <div className="rounded-xl p-4 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur-sm"
+            style={{ backgroundColor: 'rgba(255,255,255,0.8)', border: `1px solid ${COLORS.ink.medium}` }}>
+            <h2 className="font-serif text-lg font-semibold tracking-wide mb-3"
+              style={{ color: COLORS.text.primary }}>
+              专业技能
+            </h2>
             <div className="flex flex-wrap gap-2">
-              {talent.personalSkills?.split(',').map((skill: string, i: number) => (
-                <span key={i} className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm">
+              {talent.personalSkills.split(/[,,、;;]/).map((skill: string, i: number) => (
+                <span key={i}
+                  className="px-3 py-1 rounded-full text-xs transition-all duration-200 hover:shadow-md"
+                  style={{
+                    backgroundColor: COLORS.ink.medium,
+                    color: COLORS.text.primary
+                  }}>
                   {skill.trim()}
                 </span>
               ))}
             </div>
           </div>
+        )}
 
-          {/* 个人简介 */}
-          <div className="bg-white rounded-lg p-4">
-            <h2 className="font-bold text-gray-800 mb-2">个人简介</h2>
-            <p className="text-gray-600 leading-relaxed">{talent.personalIntro || '暂无简介'}</p>
+        {/* 工作经历卡片 */}
+        {talent.personalExperience && (
+          <div className="rounded-xl p-4 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur-sm"
+            style={{ backgroundColor: 'rgba(255,255,255,0.8)', border: `1px solid ${COLORS.ink.medium}` }}>
+            <h2 className="font-serif text-lg font-semibold tracking-wide mb-3"
+              style={{ color: COLORS.text.primary }}>
+              工作经历
+            </h2>
+            <p className="text-sm leading-relaxed" style={{ color: COLORS.text.secondary }}>
+              {talent.personalExperience}
+            </p>
           </div>
+        )}
 
-          {/* 个人经历 */}
-          <div className="bg-white rounded-lg p-4">
-            <h2 className="font-bold text-gray-800 mb-2">个人经历</h2>
-            <p className="text-gray-600 leading-relaxed">{talent.personalExperience || '暂无经历描述'}</p>
+        {/* 求职需求卡片 */}
+        <div className="rounded-xl p-4 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur-sm"
+          style={{ backgroundColor: 'rgba(255,255,255,0.8)', border: `1px solid ${COLORS.ink.medium}` }}>
+          <h2 className="font-serif text-lg font-semibold tracking-wide mb-3"
+            style={{ color: COLORS.text.primary }}>
+            求职信息
+          </h2>
+          
+          <div className="space-y-3">
+            <div className="flex items-center space-x-2">
+              <BriefcaseIcon className="w-4 h-4" style={{ color: COLORS.accent.blue }} />
+              <span className="text-sm" style={{ color: COLORS.text.secondary }}>
+                求职状态:{formatJobSeekingStatus(talent.jobSeekingStatus)}
+              </span>
+            </div>
+            
+            {talent.jobSeekingRequirements && (
+              <div className="flex items-start space-x-2">
+                <UserGroupIcon className="w-4 h-4 mt-0.5" style={{ color: COLORS.accent.blue }} />
+                <div>
+                  <span className="text-sm font-medium" style={{ color: COLORS.text.primary }}>求职需求:</span>
+                  <p className="text-sm mt-1" style={{ color: COLORS.text.secondary }}>
+                    {talent.jobSeekingRequirements}
+                  </p>
+                </div>
+              </div>
+            )}
           </div>
+        </div>
 
-          {/* 统计信息 */}
-          <div className="bg-white rounded-lg p-4">
-            <h2 className="font-bold text-gray-800 mb-2">统计数据</h2>
-            <div className="grid grid-cols-3 gap-4 text-center">
-              <div>
-                <p className="text-2xl font-bold text-blue-600">{talent.knowledgeReadCount || 0}</p>
-                <p className="text-sm text-gray-500">浏览量</p>
-              </div>
-              <div>
-                <p className="text-2xl font-bold text-green-600">{talent.timeBankHours || 0}h</p>
-                <p className="text-sm text-gray-500">服务时长</p>
-              </div>
-              <div>
-                <p className="text-2xl font-bold text-yellow-600">{talent.knowledgeRankingScore || 0}</p>
-                <p className="text-sm text-gray-500">综合评分</p>
-              </div>
+        {/* 认证信息卡片 */}
+        {talent.certificationInfo && (
+          <div className="rounded-xl p-4 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur-sm"
+            style={{ backgroundColor: 'rgba(255,255,255,0.8)', border: `1px solid ${COLORS.ink.medium}` }}>
+            <h2 className="font-serif text-lg font-semibold tracking-wide mb-3"
+              style={{ color: COLORS.text.primary }}>
+              认证信息
+            </h2>
+            <div className="flex items-start space-x-2">
+              <CheckCircleIcon className="w-4 h-4 mt-0.5" style={{ color: COLORS.accent.green }} />
+              <p className="text-sm leading-relaxed" style={{ color: COLORS.text.secondary }}>
+                {talent.certificationInfo}
+              </p>
+            </div>
+          </div>
+        )}
+
+        {/* 数据统计卡片 */}
+        <div className="rounded-xl p-4 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur-sm"
+          style={{ backgroundColor: 'rgba(255,255,255,0.8)', border: `1px solid ${COLORS.ink.medium}` }}>
+          <h2 className="font-serif text-lg font-semibold tracking-wide mb-4"
+            style={{ color: COLORS.text.primary }}>
+            贡献统计
+          </h2>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <div className="text-center p-3 rounded-lg" style={{ backgroundColor: 'rgba(255,255,255,0.5)' }}>
+              <p className="text-2xl font-bold" style={{ color: COLORS.accent.blue }}>{talent.totalPoints || 0}</p>
+              <p className="text-xs" style={{ color: COLORS.text.secondary }}>总积分</p>
+            </div>
+            <div className="text-center p-3 rounded-lg" style={{ backgroundColor: 'rgba(255,255,255,0.5)' }}>
+              <p className="text-2xl font-bold" style={{ color: COLORS.accent.green }}>{talent.timeBankHours || 0}h</p>
+              <p className="text-xs" style={{ color: COLORS.text.secondary }}>服务时长</p>
+            </div>
+            <div className="text-center p-3 rounded-lg" style={{ backgroundColor: 'rgba(255,255,255,0.5)' }}>
+              <p className="text-2xl font-bold" style={{ color: COLORS.accent.red }}>{talent.knowledgeContributions || 0}</p>
+              <p className="text-xs" style={{ color: COLORS.text.secondary }}>知识贡献</p>
+            </div>
+            <div className="text-center p-3 rounded-lg" style={{ backgroundColor: 'rgba(255,255,255,0.5)' }}>
+              <p className="text-2xl font-bold" style={{ color: COLORS.ink.dark }}>{talent.knowledgeRanking || 0}</p>
+              <p className="text-xs" style={{ color: COLORS.text.secondary }}>知识排名</p>
             </div>
           </div>
         </div>
-      )}
+
+        {/* 底部操作按钮 */}
+        <div className="sticky bottom-4 space-y-3">
+          <button
+            className="w-full py-3 rounded-full font-medium transition-all duration-300 hover:shadow-lg"
+            style={{ backgroundColor: COLORS.ink.dark, color: 'white' }}
+            onClick={() => talent.phone && window.open(`tel:${talent.phone}`)}
+          >
+            立即联系
+          </button>
+          
+          <div className="flex space-x-3">
+            <button
+              className="flex-1 py-2 rounded-full text-sm transition-all duration-300 hover:shadow-md"
+              style={{
+                color: COLORS.text.primary,
+                border: `1px solid ${COLORS.ink.medium}`,
+                backgroundColor: 'transparent'
+              }}
+            >
+              收藏人才
+            </button>
+            <button
+              className="flex-1 py-2 rounded-full text-sm transition-all duration-300 hover:shadow-md"
+              style={{
+                color: COLORS.text.primary,
+                border: `1px solid ${COLORS.ink.medium}`,
+                backgroundColor: 'transparent'
+              }}
+            >
+              分享名片
+            </button>
+          </div>
+        </div>
+      </div>
     </div>
   );
 };