Forráskód Böngészése

✨ feat(mobile): 完善移动端个人中心功能模块

- 重构API客户端结构,统一导出银龄用户相关API
- 新增我的收藏页面,支持知识、岗位、企业三类收藏管理
- 新增我的发布页面,展示用户发布的知识、岗位、服务案例
- 新增积分中心页面,展示积分余额、会员等级、交易流水
- 新增个人信息编辑页面,支持头像上传、基础信息修改
- 新增技能管理页面,支持技能标签和证书的管理
- 更新个人中心入口,整合所有新功能模块
- 完善路由配置,新增5个个人中心相关页面路由

♻️ refactor(api): 优化API客户端类型定义和导出方式

- 统一使用泛型定义API路由类型,提高类型安全性
- 重构axios fetch适配器,支持动态导入和错误处理
- 新增银龄岗子模块客户端导出,便于业务模块调用
yourname 8 hónapja
szülő
commit
18bcc9cb23

+ 66 - 62
src/client/api.ts

@@ -1,78 +1,82 @@
-import { hc } from 'hono/client'
-import axios from 'axios'
+import { hc } from 'hono/client';
+import type { 
+  AuthRoutes, 
+  UserRoutes, 
+  RoleRoutes, 
+  FileRoutes, 
+  SilverJobsRoutes,
+  SilverUsersRoutes,
+  ElderlyUniversityRoutes,
+  PolicyNewsRoutes,
+  UserPreferenceRoutes
+} from '@/server/api'
 
-// 创建自定义fetch函数
-const axiosFetch: typeof fetch = async (url, options) => {
-  const { method = 'GET', body, headers = {} } = options || {}
+// 创建axios fetch适配器
+const axiosFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
+  const axios = (await import('axios')).default
   
-  // 转换HeadersInit为普通对象
-  const axiosHeaders: Record<string, string> = {}
-  if (headers instanceof Headers) {
-    headers.forEach((value, key) => {
-      axiosHeaders[key] = value
-    })
-  } else if (Array.isArray(headers)) {
-    headers.forEach(([key, value]) => {
-      axiosHeaders[key] = value
-    })
-  } else if (headers) {
-    Object.assign(axiosHeaders, headers)
-  }
-
-  const config = {
-    method,
-    url: typeof url === 'string' ? url : url.toString(),
-    data: body,
-    headers: axiosHeaders,
-    timeout: 30000,
-    withCredentials: true
-  }
+  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
+  const method = init?.method || 'GET'
+  const headers = init?.headers ? Object.fromEntries(new Map(init.headers as any)) : {}
+  const data = init?.body
 
   try {
-    const response = await axios(config)
+    const response = await axios({
+      url,
+      method,
+      headers,
+      data,
+      responseType: 'json',
+      validateStatus: () => true,
+    })
+
     return new Response(JSON.stringify(response.data), {
       status: response.status,
       statusText: response.statusText,
-      headers: response.headers as any
+      headers: response.headers as any,
     })
-  } catch (error: any) {
-    console.error('API请求错误:', error)
-    return new Response(
-      JSON.stringify(error.response?.data || { message: error.message || '网络错误' }),
-      {
-        status: error.response?.status || 500,
-        statusText: error.response?.statusText || 'Internal Server Error',
-        headers: error.response?.headers as any
-      }
-    )
+  } catch (error) {
+    throw error
   }
 }
 
-// 基础客户端 - 使用any类型避免复杂类型检查
-const client = hc<any>('/api/v1', {
+// 基础客户端配置
+const baseClient = hc<{
+  api: {
+    v1: {
+      auth: AuthRoutes
+      users: UserRoutes
+      roles: RoleRoutes
+      files: FileRoutes
+      'silver-jobs': SilverJobsRoutes
+      'silver-users': SilverUsersRoutes
+      'elderly-universities': ElderlyUniversityRoutes
+      'policy-news': PolicyNewsRoutes
+      'user-preferences': UserPreferenceRoutes
+    }
+  }
+}>('/api', {
   fetch: axiosFetch,
 })
 
-// 老年大学客户端
-export const elderlyUniversityClient = client['elderly-universities']
-export const policyNewsClient = client['policy-news']
-export const userPreferenceClient = client['user-preferences']
+// 统一API客户端导出
+export const apiClient = baseClient.api.v1
 
-// 其他客户端
-export const authClient = client.auth
-export const userClient = client.users
-export const fileClient = client.files
-export const silverJobsClient = client['silver-jobs']
-export const silverUsersClient = client['silver-users']
+// 向后兼容的命名导出
+export const authClient = apiClient.auth
+export const userClient = apiClient.users
+export const roleClient = apiClient.roles
+export const fileClient = apiClient.files
+export const silverJobsClient = apiClient['silver-jobs']
+export const silverUsersClient = apiClient['silver-users']
+export const elderlyUniversityClient = apiClient['elderly-universities']
+export const policyNewsClient = apiClient['policy-news']
+export const userPreferenceClient = apiClient['user-preferences']
 
-// 默认导出
-export default {
-  auth: authClient,
-  users: userClient,
-  files: fileClient,
-  silverJobs: silverJobsClient,
-  silverUsers: silverUsersClient,
-  elderlyUniversity: elderlyUniversityClient,
-  policyNews: policyNewsClient,
-  userPreferences: userPreferenceClient
-}
+// 银龄岗子模块客户端
+export const companyClient = silverJobsClient.companies
+export const jobClient = silverJobsClient.jobs
+export const applicationClient = silverJobsClient.applications
+export const favoriteClient = silverJobsClient.favorites
+export const viewClient = silverJobsClient.views
+export const companyImageClient = silverJobsClient['company-images']

+ 265 - 0
src/client/mobile/pages/MyFavoritesPage.tsx

@@ -0,0 +1,265 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../hooks/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+import { silverUsersClient } from '@/client/api';
+import { App, Card, List, Button, Empty, Tabs, Tag } from 'antd';
+import { EyeOutlined, HeartOutlined, PhoneOutlined } from '@ant-design/icons';
+import dayjs from 'dayjs';
+
+const { TabPane } = Tabs;
+
+const MyFavoritesPage: React.FC = () => {
+  const { user } = useAuth();
+  const navigate = useNavigate();
+  const { message } = App.useApp();
+  const [loading, setLoading] = useState(false);
+  const [activeTab, setActiveTab] = useState('knowledge');
+  const [favoriteKnowledges, setFavoriteKnowledges] = useState<any[]>([]);
+  const [favoriteJobs, setFavoriteJobs] = useState<any[]>([]);
+  const [favoriteCompanies, setFavoriteCompanies] = useState<any[]>([]);
+
+  useEffect(() => {
+    if (user) {
+      loadFavorites();
+    }
+  }, [user, activeTab]);
+
+  const loadFavorites = async () => {
+    setLoading(true);
+    try {
+      if (activeTab === 'knowledge') {
+        // 加载收藏的知识
+        const response = await (silverUsersClient as any)['knowledges'].$get({
+          query: { filters: JSON.stringify({ favoritedBy: user.id }) }
+        });
+        
+        if (response.status === 200) {
+          const data = await response.json();
+          setFavoriteKnowledges(data.data || []);
+        }
+      } else if (activeTab === 'jobs') {
+        // 加载收藏的岗位
+        const response = await (silverUsersClient as any)['jobs'].$get({
+          query: { filters: JSON.stringify({ favoritedBy: user.id }) }
+        });
+        
+        if (response.status === 200) {
+          const data = await response.json();
+          setFavoriteJobs(data.data || []);
+        }
+      } else if (activeTab === 'companies') {
+        // 收藏的企业
+        const response = await (silverUsersClient as any)['companies'].$get({
+          query: { filters: JSON.stringify({ favoritedBy: user.id }) }
+        });
+        
+        if (response.status === 200) {
+          const data = await response.json();
+          setFavoriteCompanies(data.data || []);
+        }
+      }
+    } catch (error) {
+      console.error('加载收藏内容失败:', error);
+      message.error('加载收藏内容失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleRemoveFavorite = async (id: number, type: string) => {
+    try {
+      let response;
+      
+      if (type === 'knowledge') {
+        response = await (silverUsersClient as any)['knowledges'][id]['unfavorite'].$post({});
+      } else if (type === 'job') {
+        response = await (silverUsersClient as any)['jobs'][id]['unfavorite'].$post({});
+      } else if (type === 'company') {
+        response = await (silverUsersClient as any)['companies'][id]['unfavorite'].$post({});
+      }
+
+      if (response.status === 200) {
+        message.success('取消收藏成功');
+        loadFavorites();
+      }
+    } catch (error) {
+      console.error('取消收藏失败:', error);
+      message.error('取消收藏失败');
+    }
+  };
+
+  const renderKnowledgeItem = (item: any) => (
+    <List.Item
+      actions={[
+        <Button
+          key="view"
+          type="link"
+          size="small"
+          onClick={() => navigate(`/knowledge/${item.id}`)}
+        >
+          查看
+        </Button>,
+        <Button
+          key="remove"
+          type="link"
+          danger
+          size="small"
+          onClick={() => handleRemoveFavorite(item.id, 'knowledge')}
+        >
+          取消收藏
+        </Button>
+      ]}
+    >
+      <List.Item.Meta
+        title={item.title}
+        description={
+          <div>
+            <p className="text-gray-600 mb-2">{item.content.substring(0, 100)}...</p>
+            <div className="flex items-center space-x-4 text-sm text-gray-500">
+              <span><Tag>{item.categoryName}</Tag></span>
+              <span><EyeOutlined /> {item.viewCount}</span>
+              <span>{dayjs(item.createdAt).format('MM-DD HH:mm')}</span>
+            </div>
+          </div>
+        }
+      />
+    </List.Item>
+  );
+
+  const renderJobItem = (item: any) => (
+    <List.Item
+      actions={[
+        <Button
+          key="view"
+          type="link"
+          size="small"
+          onClick={() => navigate(`/jobs/${item.id}`)}
+        >
+          查看
+        </Button>,
+        <Button
+          key="remove"
+          type="link"
+          danger
+          size="small"
+          onClick={() => handleRemoveFavorite(item.id, 'job')}
+        >
+          取消收藏
+        </Button>
+      ]}
+    >
+      <List.Item.Meta
+        title={
+          <div className="flex justify-between items-start">
+            <span className="font-medium">{item.title}</span>
+            <Tag color="success">{item.salary}</Tag>
+          </div>
+        }
+        description={
+          <div>
+            <p className="text-gray-600 mb-2">{item.description.substring(0, 100)}...</p>
+            <div className="flex items-center space-x-4 text-sm text-gray-500">
+              <span>{item.location}</span>
+              <span>{item.experience}</span>
+              <span>{dayjs(item.createdAt).format('MM-DD HH:mm')}</span>
+            </div>
+          </div>
+        }
+      />
+    </List.Item>
+  );
+
+  const renderCompanyItem = (item: any) => (
+    <List.Item
+      actions={[
+        <Button
+          key="view"
+          type="link"
+          size="small"
+          onClick={() => navigate(`/companies/${item.id}`)}
+        >
+          查看
+        </Button>,
+        <Button
+          key="remove"
+          type="link"
+          danger
+          size="small"
+          onClick={() => handleRemoveFavorite(item.id, 'company')}
+        >
+          取消收藏
+        </Button>
+      ]}
+    >
+      <List.Item.Meta
+        title={item.name}
+        description={
+          <div>
+            <p className="text-gray-600 mb-2">{item.description.substring(0, 100)}...</p>
+            <div className="flex items-center space-x-4 text-sm text-gray-500">
+              <span>{item.industry}</span>
+              <span>{item.scale}</span>
+              <span><PhoneOutlined /> {item.contact}</span>
+            </div>
+          </div>
+        }
+      />
+    </List.Item>
+  );
+
+  if (!user) {
+    return (
+      <div className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <div className="text-center">
+          <Empty description="请登录查看收藏" />
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="bg-white">
+        <div className="flex items-center p-4 border-b">
+          <Button type="text" onClick={() => navigate('/profile')}>
+            返回
+          </Button>
+          <h1 className="flex-1 text-center text-lg font-semibold">我的收藏</h1>
+        </div>
+      </div>
+
+      <div className="p-4">
+        <Tabs activeKey={activeTab} onChange={setActiveTab}>
+          <TabPane tab="知识分享" key="knowledge">
+            <List
+              loading={loading}
+              dataSource={favoriteKnowledges}
+              renderItem={renderKnowledgeItem}
+              locale={{ emptyText: <Empty description="暂无收藏的知识" /> }}
+            />
+          </TabPane>
+          
+          <TabPane tab="岗位信息" key="jobs">
+            <List
+              loading={loading}
+              dataSource={favoriteJobs}
+              renderItem={renderJobItem}
+              locale={{ emptyText: <Empty description="暂无收藏的岗位" /> }}
+            />
+          </TabPane>
+          
+          <TabPane tab="企业信息" key="companies">
+            <List
+              loading={loading}
+              dataSource={favoriteCompanies}
+              renderItem={renderCompanyItem}
+              locale={{ emptyText: <Empty description="暂无收藏的企业" /> }}
+            />
+          </TabPane>
+        </Tabs>
+      </div>
+    </div>
+  );
+};
+
+export default MyFavoritesPage;

+ 315 - 0
src/client/mobile/pages/MyPostsPage.tsx

@@ -0,0 +1,315 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../hooks/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+import { silverUsersClient } from '@/client/api';
+import { App, Card, List, Tag, Button, Empty, Tabs, Avatar } from 'antd';
+import { EyeOutlined, LikeOutlined, MessageOutlined, DeleteOutlined } from '@ant-design/icons';
+import dayjs from 'dayjs';
+
+const { TabPane } = Tabs;
+
+interface PostItem {
+  id: number;
+  title: string;
+  content: string;
+  categoryName: string;
+  viewCount: number;
+  likeCount: number;
+  commentCount: number;
+  status: string;
+  createdAt: string;
+  coverImage?: string;
+}
+
+const MyPostsPage: React.FC = () => {
+  const { user } = useAuth();
+  const navigate = useNavigate();
+  const { message } = App.useApp();
+  const [loading, setLoading] = useState(false);
+  const [activeTab, setActiveTab] = useState('knowledge');
+  const [knowledgePosts, setKnowledgePosts] = useState<PostItem[]>([]);
+  const [jobPosts, setJobPosts] = useState<any[]>([]);
+  const [servicePosts, setServicePosts] = useState<any[]>([]);
+
+  useEffect(() => {
+    if (user) {
+      loadPosts();
+    }
+  }, [user, activeTab]);
+
+  const loadPosts = async () => {
+    setLoading(true);
+    try {
+      if (activeTab === 'knowledge') {
+        // 加载知识分享
+        const response = await (silverUsersClient as any)['knowledges'].$get({
+          query: { filters: JSON.stringify({ authorId: user.id }) }
+        });
+        
+        if (response.status === 200) {
+          const data = await response.json();
+          setKnowledgePosts(data.data || []);
+        }
+      } else if (activeTab === 'jobs') {
+        // 加载岗位发布
+        const response = await (silverUsersClient as any)['jobs'].$get({
+          query: { filters: JSON.stringify({ userId: user.id }) }
+        });
+        
+        if (response.status === 200) {
+          const data = await response.json();
+          setJobPosts(data.data || []);
+        }
+      } else if (activeTab === 'services') {
+        // 加载服务案例
+        const response = await (silverUsersClient as any)['time-banks'].$get({
+          query: { filters: JSON.stringify({ userId: user.id }) }
+        });
+        
+        if (response.status === 200) {
+          const data = await response.json();
+          setServicePosts(data.data || []);
+        }
+      }
+    } catch (error) {
+      console.error('加载发布内容失败:', error);
+      message.error('加载发布内容失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleDelete = async (id: number, type: string) => {
+    try {
+      let response;
+      
+      if (type === 'knowledge') {
+        response = await (silverUsersClient as any)['knowledges'][id].$delete();
+      } else if (type === 'job') {
+        response = await (silverUsersClient as any)['jobs'][id].$delete();
+      } else if (type === 'service') {
+        response = await (silverUsersClient as any)['time-banks'][id].$delete();
+      }
+
+      if (response.status === 200) {
+        message.success('删除成功');
+        loadPosts();
+      }
+    } catch (error) {
+      console.error('删除失败:', error);
+      message.error('删除失败');
+    }
+  };
+
+  const getStatusText = (status: string) => {
+    const statusMap: Record<string, { text: string; color: string }> = {
+      'published': { text: '已发布', color: 'success' },
+      'pending': { text: '审核中', color: 'processing' },
+      'rejected': { text: '已拒绝', color: 'error' },
+      'draft': { text: '草稿', color: 'default' }
+    };
+    return statusMap[status] || { text: status, color: 'default' };
+  };
+
+  const renderKnowledgeItem = (item: PostItem) => (
+    <List.Item
+      actions={[
+        <Button
+          key="edit"
+          type="link"
+          size="small"
+          onClick={() => navigate(`/knowledge/edit/${item.id}`)}
+        >
+          编辑
+        </Button>,
+        <Button
+          key="delete"
+          type="link"
+          danger
+          size="small"
+          onClick={() => handleDelete(item.id, 'knowledge')}
+        >
+          删除
+        </Button>
+      ]}
+    >
+      <List.Item.Meta
+        avatar={
+          item.coverImage ? (
+            <Avatar shape="square" size={64} src={item.coverImage} />
+          ) : (
+            <Avatar shape="square" size={64} style={{ backgroundColor: '#1890ff' }}>
+              {item.title.charAt(0)}
+            </Avatar>
+          )
+        }
+        title={
+          <div className="flex justify-between items-start">
+            <span className="font-medium">{item.title}</span>
+            <Tag color={getStatusText(item.status).color}>
+              {getStatusText(item.status).text}
+            </Tag>
+          </div>
+        }
+        description={
+          <div>
+            <p className="text-gray-600 mb-2">{item.content.substring(0, 100)}...</p>
+            <div className="flex items-center space-x-4 text-sm text-gray-500">
+              <span><Tag>{item.categoryName}</Tag></span>
+              <span><EyeOutlined /> {item.viewCount}</span>
+              <span><LikeOutlined /> {item.likeCount}</span>
+              <span><MessageOutlined /> {item.commentCount}</span>
+              <span>{dayjs(item.createdAt).format('MM-DD HH:mm')}</span>
+            </div>
+          </div>
+        }
+      />
+    </List.Item>
+  );
+
+  const renderJobItem = (item: any) => (
+    <List.Item
+      actions={[
+        <Button
+          key="edit"
+          type="link"
+          size="small"
+          onClick={() => navigate(`/jobs/edit/${item.id}`)}
+        >
+          编辑
+        </Button>,
+        <Button
+          key="delete"
+          type="link"
+          danger
+          size="small"
+          onClick={() => handleDelete(item.id, 'job')}
+        >
+          删除
+        </Button>
+      ]}
+    >
+      <List.Item.Meta
+        title={
+          <div className="flex justify-between items-start">
+            <span className="font-medium">{item.title}</span>
+            <Tag color={getStatusText(item.status).color}>
+              {getStatusText(item.status).text}
+            </Tag>
+          </div>
+        }
+        description={
+          <div>
+            <p className="text-gray-600 mb-2">{item.description.substring(0, 100)}...</p>
+            <div className="flex items-center space-x-4 text-sm text-gray-500">
+              <span>{item.location}</span>
+              <span>{item.salary}</span>
+              <span>{dayjs(item.createdAt).format('MM-DD HH:mm')}</span>
+            </div>
+          </div>
+        }
+      />
+    </List.Item>
+  );
+
+  const renderServiceItem = (item: any) => (
+    <List.Item
+      actions={[
+        <Button
+          key="edit"
+          type="link"
+          size="small"
+          onClick={() => navigate(`/services/edit/${item.id}`)}
+        >
+          编辑
+        </Button>,
+        <Button
+          key="delete"
+          type="link"
+          danger
+          size="small"
+          onClick={() => handleDelete(item.id, 'service')}
+        >
+          删除
+        </Button>
+      ]}
+    >
+      <List.Item.Meta
+        title={
+          <div className="flex justify-between items-start">
+            <span className="font-medium">{item.title}</span>
+            <Tag color={getStatusText(item.status).color}>
+              {getStatusText(item.status).text}
+            </Tag>
+          </div>
+        }
+        description={
+          <div>
+            <p className="text-gray-600 mb-2">{item.description.substring(0, 100)}...</p>
+            <div className="flex items-center space-x-4 text-sm text-gray-500">
+              <span>{item.serviceType}</span>
+              <span>{item.duration}小时</span>
+              <span>{dayjs(item.createdAt).format('MM-DD HH:mm')}</span>
+            </div>
+          </div>
+        }
+      />
+    </List.Item>
+  );
+
+  if (!user) {
+    return (
+      <div className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <div className="text-center">
+          <Empty description="请登录查看发布内容" />
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="bg-white">
+        <div className="flex items-center p-4 border-b">
+          <Button type="text" onClick={() => navigate('/profile')}>
+            返回
+          </Button>
+          <h1 className="flex-1 text-center text-lg font-semibold">我的发布</h1>
+        </div>
+      </div>
+
+      <div className="p-4">
+        <Tabs activeKey={activeTab} onChange={setActiveTab}>
+          <TabPane tab="知识分享" key="knowledge">
+            <List
+              loading={loading}
+              dataSource={knowledgePosts}
+              renderItem={renderKnowledgeItem}
+              locale={{ emptyText: <Empty description="暂无知识分享" /> }}
+            />
+          </TabPane>
+          
+          <TabPane tab="岗位发布" key="jobs">
+            <List
+              loading={loading}
+              dataSource={jobPosts}
+              renderItem={renderJobItem}
+              locale={{ emptyText: <Empty description="暂无岗位发布" /> }}
+            />
+          </TabPane>
+          
+          <TabPane tab="服务案例" key="services">
+            <List
+              loading={loading}
+              dataSource={servicePosts}
+              renderItem={renderServiceItem}
+              locale={{ emptyText: <Empty description="暂无服务案例" /> }}
+            />
+          </TabPane>
+        </Tabs>
+      </div>
+    </div>
+  );
+};
+
+export default MyPostsPage;

+ 217 - 0
src/client/mobile/pages/PointsPage.tsx

@@ -0,0 +1,217 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../hooks/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+import { silverUsersClient } from '@/client/api';
+import { App, Card, List, Tag, Empty, Statistic } from 'antd';
+import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
+
+const PointsPage: React.FC = () => {
+  const { user } = useAuth();
+  const navigate = useNavigate();
+  const { message } = App.useApp();
+  const [loading, setLoading] = useState(false);
+  const [points, setPoints] = useState<any>(null);
+  const [transactions, setTransactions] = useState<any[]>([]);
+  const [stats, setStats] = useState({
+    totalEarned: 0,
+    totalSpent: 0,
+    currentBalance: 0
+  });
+
+  useEffect(() => {
+    if (user) {
+      loadPointsData();
+    }
+  }, [user]);
+
+  const loadPointsData = async () => {
+    setLoading(true);
+    try {
+      // 加载积分信息
+      const pointsRes = await (silverUsersClient as any)['points'].$get({
+        query: { filters: JSON.stringify({ userId: user.id }) }
+      });
+      
+      if (pointsRes.status === 200) {
+        const pointsData = await pointsRes.json();
+        if (pointsData.data && pointsData.data.length > 0) {
+          setPoints(pointsData.data[0]);
+          setStats(prev => ({ ...prev, currentBalance: pointsData.data[0].balance }));
+        }
+      }
+
+      // 加载积分流水
+      const transactionsRes = await (silverUsersClient as any)['points']['transactions'].$get({
+        query: { filters: JSON.stringify({ userId: user.id }) }
+      });
+      
+      if (transactionsRes.status === 200) {
+        const transactionsData = await transactionsRes.json();
+        setTransactions(transactionsData.data || []);
+        
+        // 计算统计信息
+        const earned = transactionsData.data
+          ?.filter((t: any) => t.type === 'earn')
+          .reduce((sum: number, t: any) => sum + t.points, 0) || 0;
+        
+        const spent = transactionsData.data
+          ?.filter((t: any) => t.type === 'spend')
+          .reduce((sum: number, t: any) => sum + Math.abs(t.points), 0) || 0;
+        
+        setStats({ totalEarned: earned, totalSpent: spent, currentBalance: stats.currentBalance });
+      }
+    } catch (error) {
+      console.error('加载积分数据失败:', error);
+      message.error('加载积分数据失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const getTransactionTypeText = (type: string) => {
+    const typeMap: Record<string, string> = {
+      'earn': '获得',
+      'spend': '消费',
+      'refund': '退款',
+      'reward': '奖励',
+      'penalty': '扣除'
+    };
+    return typeMap[type] || type;
+  };
+
+  const getTransactionTypeColor = (type: string) => {
+    const colorMap: Record<string, string> = {
+      'earn': 'success',
+      'spend': 'error',
+      'refund': 'processing',
+      'reward': 'success',
+      'penalty': 'warning'
+    };
+    return colorMap[type] || 'default';
+  };
+
+  const getLevelTag = (level: string) => {
+    const levelMap: Record<string, { color: string; text: string }> = {
+      'bronze': { color: 'orange', text: '青铜会员' },
+      'silver': { color: 'gray', text: '白银会员' },
+      'gold': { color: 'gold', text: '黄金会员' },
+      'platinum': { color: 'blue', text: '铂金会员' },
+      'diamond': { color: 'purple', text: '钻石会员' }
+    };
+    return levelMap[level] || { color: 'default', text: '普通会员' };
+  };
+
+  if (!user) {
+    return (
+      <div className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <div className="text-center">
+          <p className="text-gray-600 mb-4">请先登录</p>
+          <Empty description="请登录查看积分" />
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="bg-white">
+        <div className="flex items-center p-4 border-b">
+          <Button type="text" onClick={() => navigate('/profile')}>
+            返回
+          </Button>
+          <h1 className="flex-1 text-center text-lg font-semibold">积分中心</h1>
+        </div>
+      </div>
+
+      {/* 积分概览 */}
+      <div className="p-4">
+        <div className="grid grid-cols-3 gap-4 mb-6">
+          <Card className="text-center">
+            <Statistic
+              title="当前积分"
+              value={stats.currentBalance}
+              valueStyle={{ color: '#3f8600' }}
+              prefix={<ArrowUpOutlined />}
+            />
+          </Card>
+          <Card className="text-center">
+            <Statistic
+              title="累计获得"
+              value={stats.totalEarned}
+              valueStyle={{ color: '#3f8600' }}
+            />
+          </Card>
+          <Card className="text-center">
+            <Statistic
+              title="累计消费"
+              value={stats.totalSpent}
+              valueStyle={{ color: '#cf1322' }}
+              prefix={<ArrowDownOutlined />}
+            />
+          </Card>
+        </div>
+
+        {/* 会员等级 */}
+        {points && (
+          <Card className="mb-6">
+            <div className="flex items-center justify-between">
+              <div>
+                <h3 className="text-lg font-semibold">会员等级</h3>
+                <p className="text-gray-600">享受专属权益</p>
+              </div>
+              <Tag 
+                color={getLevelTag(points.level).color}
+                className="px-4 py-2 text-lg"
+              >
+                {getLevelTag(points.level).text}
+              </Tag>
+            </div>
+            <div className="mt-4">
+              <div className="flex justify-between text-sm text-gray-600 mb-1">
+                <span>升级进度</span>
+                <span>{points.points} / {points.nextLevelPoints}</span>
+              </div>
+              <div className="w-full bg-gray-200 rounded-full h-2">
+                <div 
+                  className="bg-blue-600 h-2 rounded-full" 
+                  style={{ width: `${Math.min((points.points / points.nextLevelPoints) * 100, 100)}%` }}
+                ></div>
+              </div>
+            </div>
+          </Card>
+        )}
+
+        {/* 积分流水 */}
+        <Card title="积分流水">
+          <List
+            loading={loading}
+            dataSource={transactions}
+            renderItem={(item: any) => (
+              <List.Item>
+                <List.Item.Meta
+                  title={
+                    <div className="flex justify-between items-center">
+                      <span>{item.description}</span>
+                      <Tag color={getTransactionTypeColor(item.type)}>
+                        {item.points > 0 ? '+' : ''}{item.points}
+                      </Tag>
+                    </div>
+                  }
+                  description={
+                    <div>
+                      <p className="text-gray-600">{getTransactionTypeText(item.type)}</p>
+                      <p className="text-gray-400 text-sm">{dayjs(item.createdAt).format('YYYY-MM-DD HH:mm')}</p>
+                    </div>
+                  }
+                />
+              </List.Item>
+            )}
+            locale={{ emptyText: <Empty description="暂无积分记录" /> }}
+          />
+        </Card>
+      </div>
+    </div>
+  );
+};
+
+export default PointsPage;

+ 256 - 0
src/client/mobile/pages/ProfileEditPage.tsx

@@ -0,0 +1,256 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../hooks/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+import { silverUsersClient, userClient } from '@/client/api';
+import { App, Form, Input, Button, DatePicker, Select, Upload } from 'antd';
+import { UploadOutlined } from '@ant-design/icons';
+import dayjs from 'dayjs';
+import type { UploadProps } from 'antd';
+
+const { Option } = Select;
+
+const ProfileEditPage: React.FC = () => {
+  const { user } = useAuth();
+  const navigate = useNavigate();
+  const { message } = App.useApp();
+  const [form] = Form.useForm();
+  const [loading, setLoading] = useState(false);
+  const [profile, setProfile] = useState<any>(null);
+  const [avatarUrl, setAvatarUrl] = useState<string>('');
+
+  useEffect(() => {
+    if (user) {
+      loadProfile();
+    }
+  }, [user]);
+
+  const loadProfile = async () => {
+    try {
+      const response = await (silverUsersClient as any)['profiles'].$get({
+        query: { filters: JSON.stringify({ userId: user.id }) }
+      });
+      
+      if (response.status === 200) {
+        const data = await response.json();
+        if (data.data && data.data.length > 0) {
+          const profileData = data.data[0];
+          setProfile(profileData);
+          setAvatarUrl(profileData.avatar || '');
+          
+          form.setFieldsValue({
+            ...profileData,
+            birthDate: profileData.birthDate ? dayjs(profileData.birthDate) : null,
+            workStartDate: profileData.workStartDate ? dayjs(profileData.workStartDate) : null
+          });
+        } else {
+          form.setFieldsValue({
+            userId: user.id,
+            phone: user.phone || ''
+          });
+        }
+        
+        form.setFieldsValue({
+          username: user.username,
+          email: user.email
+        });
+      }
+    } catch (error) {
+      console.error('加载个人档案失败:', error);
+      message.error('加载个人档案失败');
+    }
+  };
+
+  const handleAvatarUpload: UploadProps['customRequest'] = async (options) => {
+    const { file, onSuccess, onError } = options;
+    
+    try {
+      const formData = new FormData();
+      formData.append('file', file);
+      
+      const response = await fetch('/api/files/upload', {
+        method: 'POST',
+        body: formData
+      });
+      
+      const data = await response.json();
+      if (data.url) {
+        setAvatarUrl(data.url);
+        onSuccess?.(data);
+      }
+    } catch (error) {
+      onError?.(error as Error);
+    }
+  };
+
+  const handleSubmit = async (values: any) => {
+    if (!user) return;
+    
+    setLoading(true);
+    try {
+      // 更新用户信息
+      const userUpdateResponse = await (userClient as any)[user.id].$put({
+        json: {
+          username: values.username,
+          email: values.email,
+          phone: values.phone
+        }
+      });
+
+      if (userUpdateResponse.status === 200) {
+        // 更新银龄档案
+        const profileData = {
+          ...values,
+          avatar: avatarUrl,
+          birthDate: values.birthDate?.format('YYYY-MM-DD'),
+          workStartDate: values.workStartDate?.format('YYYY-MM-DD'),
+          userId: user.id
+        };
+
+        let response;
+        if (profile?.id) {
+          response = await (silverUsersClient as any)['profiles'][profile.id].$put({
+            json: profileData
+          });
+        } else {
+          response = await (silverUsersClient as any)['profiles'].$post({
+            json: profileData
+          });
+        }
+
+        if (response.status === 200) {
+          message.success('个人信息更新成功');
+          navigate('/profile');
+        }
+      }
+    } catch (error) {
+      console.error('更新失败:', error);
+      message.error('更新失败,请重试');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!user) {
+    return (
+      <div className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <div className="text-center">
+          <p className="text-gray-600 mb-4">请先登录</p>
+          <Button type="primary" onClick={() => navigate('/login')}>
+            去登录
+          </Button>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="bg-white">
+        <div className="flex items-center p-4 border-b">
+          <Button type="text" onClick={() => navigate('/profile')}>
+            返回
+          </Button>
+          <h1 className="flex-1 text-center text-lg font-semibold">编辑个人信息</h1>
+        </div>
+      </div>
+
+      <div className="p-4">
+        <Form form={form} layout="vertical" onFinish={handleSubmit}>
+          {/* 头像上传 */}
+          <Form.Item label="头像">
+            <Upload
+              customRequest={handleAvatarUpload}
+              showUploadList={false}
+              accept="image/*"
+            >
+              <div className="flex items-center space-x-4">
+                {avatarUrl ? (
+                  <img src={avatarUrl} alt="头像" className="w-20 h-20 rounded-full object-cover" />
+                ) : (
+                  <div className="w-20 h-20 bg-gray-200 rounded-full flex items-center justify-center">
+                    <UploadOutlined className="text-2xl text-gray-400" />
+                  </div>
+                )}
+                <Button icon={<UploadOutlined />}>上传头像</Button>
+              </div>
+            </Upload>
+          </Form.Item>
+
+          {/* 基础信息 */}
+          <Form.Item
+            name="username"
+            label="用户名"
+            rules={[{ required: true, message: '请输入用户名' }]}
+          >
+            <Input placeholder="请输入用户名" />
+          </Form.Item>
+
+          <Form.Item
+            name="email"
+            label="邮箱"
+            rules={[{ required: true, type: 'email', message: '请输入有效邮箱' }]}
+          >
+            <Input placeholder="请输入邮箱" />
+          </Form.Item>
+
+          <Form.Item
+            name="phone"
+            label="手机号"
+            rules={[{ required: true, message: '请输入手机号' }]}
+          >
+            <Input placeholder="请输入手机号" />
+          </Form.Item>
+
+          {/* 银龄档案信息 */}
+          <Form.Item name="realName" label="真实姓名">
+            <Input placeholder="请输入真实姓名" />
+          </Form.Item>
+
+          <Form.Item name="gender" label="性别">
+            <Select placeholder="请选择性别">
+              <Option value="male">男</Option>
+              <Option value="female">女</Option>
+              <Option value="other">其他</Option>
+            </Select>
+          </Form.Item>
+
+          <Form.Item name="birthDate" label="出生日期">
+            <DatePicker format="YYYY-MM-DD" placeholder="请选择出生日期" />
+          </Form.Item>
+
+          <Form.Item name="age" label="年龄">
+            <Input type="number" placeholder="请输入年龄" />
+          </Form.Item>
+
+          <Form.Item name="location" label="所在地">
+            <Input placeholder="请输入所在地" />
+          </Form.Item>
+
+          <Form.Item name="workStartDate" label="工作起始年份">
+            <DatePicker picker="year" placeholder="请选择工作起始年份" />
+          </Form.Item>
+
+          <Form.Item name="workExperience" label="工作年限">
+            <Input type="number" placeholder="请输入工作年限" />
+          </Form.Item>
+
+          <Form.Item name="expectedSalary" label="期望薪资">
+            <Input placeholder="请输入期望薪资范围" />
+          </Form.Item>
+
+          <Form.Item name="introduction" label="个人简介">
+            <Input.TextArea rows={4} placeholder="请简单介绍自己" maxLength={500} />
+          </Form.Item>
+
+          <Form.Item>
+            <Button type="primary" htmlType="submit" loading={loading} block>
+              保存修改
+            </Button>
+          </Form.Item>
+        </Form>
+      </div>
+    </div>
+  );
+};
+
+export default ProfileEditPage;

+ 44 - 8
src/client/mobile/pages/ProfilePage.tsx

@@ -3,7 +3,7 @@ import { useAuth } from '../hooks/AuthProvider';
 import { useNavigate } from 'react-router-dom';
 import { userPreferenceClient } from '@/client/api';
 import { FontSizeType } from '@/server/modules/silver-users/user-preference.entity';
-import { App } from 'antd';
+import { App, Card, Button } from 'antd';
 
 const fontSizeOptions = [
   { value: FontSizeType.SMALL, label: '小' },
@@ -154,10 +154,30 @@ const ProfilePage: React.FC = () => {
         </div>
       </div>
 
+      {/* 银龄档案信息 */}
+      <Card className="mb-4">
+        <div className="flex justify-between items-center mb-4">
+          <h3 className="text-lg font-semibold text-gray-900">银龄档案</h3>
+          <Button type="link" onClick={() => navigate('/profile/edit')}>
+            完善信息
+          </Button>
+        </div>
+        <div className="grid grid-cols-2 gap-4">
+          <div>
+            <p className="text-sm text-gray-600">积分余额</p>
+            <p className="text-xl font-bold text-blue-600">--</p>
+          </div>
+          <div>
+            <p className="text-sm text-gray-600">认证状态</p>
+            <p className="text-sm text-green-600">已认证</p>
+          </div>
+        </div>
+      </Card>
+
       {/* 功能菜单 */}
       <div className="bg-white rounded-lg shadow">
         <div className="divide-y divide-gray-200">
-          <div 
+          <div
             className="p-4 flex items-center justify-between hover:bg-gray-50 cursor-pointer"
             onClick={() => navigate('/profile/edit')}
           >
@@ -165,23 +185,39 @@ const ProfilePage: React.FC = () => {
             <span className="text-gray-400">›</span>
           </div>
           
-          <div 
+          <div
             className="p-4 flex items-center justify-between hover:bg-gray-50 cursor-pointer"
-            onClick={() => navigate('/my-posts')}
+            onClick={() => navigate('/profile/points')}
+          >
+            <span className="text-gray-900">积分中心</span>
+            <span className="text-gray-400">›</span>
+          </div>
+          
+          <div
+            className="p-4 flex items-center justify-between hover:bg-gray-50 cursor-pointer"
+            onClick={() => navigate('/profile/posts')}
           >
             <span className="text-gray-900">我的发布</span>
             <span className="text-gray-400">›</span>
           </div>
           
-          <div 
+          <div
+            className="p-4 flex items-center justify-between hover:bg-gray-50 cursor-pointer"
+            onClick={() => navigate('/profile/favorites')}
+          >
+            <span className="text-gray-900">我的收藏</span>
+            <span className="text-gray-400">›</span>
+          </div>
+          
+          <div
             className="p-4 flex items-center justify-between hover:bg-gray-50 cursor-pointer"
-            onClick={() => navigate('/settings')}
+            onClick={() => navigate('/profile/skills')}
           >
-            <span className="text-gray-900">设置</span>
+            <span className="text-gray-900">技能管理</span>
             <span className="text-gray-400">›</span>
           </div>
           
-          <div 
+          <div
             className="p-4 flex items-center justify-between hover:bg-gray-50 cursor-pointer text-red-600"
             onClick={handleLogout}
           >

+ 358 - 0
src/client/mobile/pages/SkillsPage.tsx

@@ -0,0 +1,358 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../hooks/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+import { silverUsersClient } from '@/client/api';
+import { App, Card, List, Button, Tag, Modal, Form, Input, Select, Upload, message } from 'antd';
+import { PlusOutlined, UploadOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
+import type { UploadProps } from 'antd';
+
+const { Option } = Select;
+
+const SkillsPage: React.FC = () => {
+  const { user } = useAuth();
+  const navigate = useNavigate();
+  const { message } = App.useApp();
+  const [loading, setLoading] = useState(false);
+  const [skills, setSkills] = useState<any[]>([]);
+  const [certificates, setCertificates] = useState<any[]>([]);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [modalType, setModalType] = useState<'skill' | 'certificate'>('skill');
+  const [editingItem, setEditingItem] = useState<any>(null);
+  const [form] = Form.useForm();
+
+  useEffect(() => {
+    if (user) {
+      loadSkills();
+    }
+  }, [user]);
+
+  const loadSkills = async () => {
+    setLoading(true);
+    try {
+      // 加载技能标签
+      const response = await (silverUsersClient as any)['profiles'].$get({
+        query: { filters: JSON.stringify({ userId: user.id }) }
+      });
+      
+      if (response.status === 200) {
+        const data = await response.json();
+        if (data.data && data.data.length > 0) {
+          const profile = data.data[0];
+          setSkills(profile.skills?.split(',') || []);
+          setCertificates(profile.certificates || []);
+        }
+      }
+    } catch (error) {
+      console.error('加载技能信息失败:', error);
+      message.error('加载技能信息失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleAddSkill = async (values: any) => {
+    try {
+      const newSkills = [...skills, values.skill];
+      await updateProfile({ skills: newSkills.join(',') });
+      setSkills(newSkills);
+      message.success('技能添加成功');
+    } catch (error) {
+      message.error('技能添加失败');
+    }
+  };
+
+  const handleRemoveSkill = async (skillToRemove: string) => {
+    try {
+      const newSkills = skills.filter(skill => skill !== skillToRemove);
+      await updateProfile({ skills: newSkills.join(',') });
+      setSkills(newSkills);
+      message.success('技能删除成功');
+    } catch (error) {
+      message.error('技能删除失败');
+    }
+  };
+
+  const handleAddCertificate = async (values: any) => {
+    try {
+      const newCertificates = [...certificates, values];
+      await updateProfile({ certificates: newCertificates });
+      setCertificates(newCertificates);
+      message.success('证书添加成功');
+    } catch (error) {
+      message.error('证书添加失败');
+    }
+  };
+
+  const handleRemoveCertificate = async (index: number) => {
+    try {
+      const newCertificates = certificates.filter((_, i) => i !== index);
+      await updateProfile({ certificates: newCertificates });
+      setCertificates(newCertificates);
+      message.success('证书删除成功');
+    } catch (error) {
+      message.error('证书删除失败');
+    }
+  };
+
+  const updateProfile = async (data: any) => {
+    const response = await (silverUsersClient as any)['profiles'].$get({
+      query: { filters: JSON.stringify({ userId: user.id }) }
+    });
+    
+    if (response.status === 200) {
+      const profileData = await response.json();
+      const profile = profileData.data[0];
+      
+      if (profile?.id) {
+        await (silverUsersClient as any)['profiles'][profile.id].$put({
+          json: { ...profile, ...data }
+        });
+      } else {
+        await (silverUsersClient as any)['profiles'].$post({
+          json: { userId: user.id, ...data }
+        });
+      }
+    }
+  };
+
+  const handleUploadCertificate: UploadProps['customRequest'] = async (options) => {
+    const { file, onSuccess, onError } = options;
+    
+    try {
+      const formData = new FormData();
+      formData.append('file', file);
+      
+      const response = await fetch('/api/files/upload', {
+        method: 'POST',
+        body: formData
+      });
+      
+      const data = await response.json();
+      if (data.url) {
+        form.setFieldsValue({ imageUrl: data.url });
+        onSuccess?.(data);
+      }
+    } catch (error) {
+      onError?.(error as Error);
+    }
+  };
+
+  const showModal = (type: 'skill' | 'certificate', item?: any) => {
+    setModalType(type);
+    setEditingItem(item);
+    setModalVisible(true);
+    
+    if (type === 'certificate' && item) {
+      form.setFieldsValue(item);
+    } else {
+      form.resetFields();
+    }
+  };
+
+  const handleModalSubmit = async (values: any) => {
+    if (modalType === 'skill') {
+      await handleAddSkill(values);
+    } else {
+      await handleAddCertificate(values);
+    }
+    setModalVisible(false);
+  };
+
+  const skillCategories = [
+    '医疗健康',
+    '教育培训',
+    '农业技术',
+    '生活技能',
+    '艺术文化',
+    '体育健身',
+    '科技应用',
+    '心理咨询',
+    '法律咨询',
+    '财务管理'
+  ];
+
+  if (!user) {
+    return (
+      <div className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <div className="text-center">
+          <Empty description="请登录管理技能" />
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="bg-white">
+        <div className="flex items-center p-4 border-b">
+          <Button type="text" onClick={() => navigate('/profile')}>
+            返回
+          </Button>
+          <h1 className="flex-1 text-center text-lg font-semibold">技能管理</h1>
+        </div>
+      </div>
+
+      <div className="p-4 space-y-6">
+        {/* 技能标签 */}
+        <Card 
+          title="我的技能标签" 
+          extra={
+            <Button 
+              type="primary" 
+              icon={<PlusOutlined />}
+              onClick={() => showModal('skill')}
+            >
+              添加技能
+            </Button>
+          }
+        >
+          {skills.length > 0 ? (
+            <div className="flex flex-wrap gap-2">
+              {skills.map((skill, index) => (
+                <Tag
+                  key={index}
+                  closable
+                  onClose={() => handleRemoveSkill(skill)}
+                  className="px-3 py-1"
+                >
+                  {skill}
+                </Tag>
+              ))}
+            </div>
+          ) : (
+            <Empty description="暂无技能标签" />
+          )}
+        </Card>
+
+        {/* 技能证书 */}
+        <Card 
+          title="技能证书" 
+          extra={
+            <Button 
+              type="primary" 
+              icon={<PlusOutlined />}
+              onClick={() => showModal('certificate')}
+            >
+              添加证书
+            </Button>
+          }
+        >
+          <List
+            dataSource={certificates}
+            renderItem={(item, index) => (
+              <List.Item
+                actions={[
+                  <Button
+                    key="edit"
+                    type="text"
+                    icon={<EditOutlined />}
+                    onClick={() => showModal('certificate', item)}
+                  />,
+                  <Button
+                    key="delete"
+                    type="text"
+                    danger
+                    icon={<DeleteOutlined />}
+                    onClick={() => handleRemoveCertificate(index)}
+                  />
+                ]}
+              >
+                <List.Item.Meta
+                  title={item.name}
+                  description={
+                    <div>
+                      <p>{item.description}</p>
+                      <p className="text-gray-500 text-sm">
+                        颁发机构: {item.issuingAgency} | 有效期: {item.validityPeriod}
+                      </p>
+                      {item.imageUrl && (
+                        <img 
+                          src={item.imageUrl} 
+                          alt={item.name} 
+                          className="w-32 h-20 object-cover rounded mt-2"
+                        />
+                      )}
+                    </div>
+                  }
+                />
+              </List.Item>
+            )}
+            locale={{ emptyText: <Empty description="暂无技能证书" /> }}
+          />
+        </Card>
+
+        {/* 添加模态框 */}
+        <Modal
+          title={modalType === 'skill' ? '添加技能' : '添加证书'}
+          open={modalVisible}
+          onCancel={() => setModalVisible(false)}
+          onOk={() => form.submit()}
+          okText="保存"
+          cancelText="取消"
+        >
+          <Form form={form} onFinish={handleModalSubmit} layout="vertical">
+            {modalType === 'skill' ? (
+              <Form.Item
+                name="skill"
+                label="技能名称"
+                rules={[{ required: true, message: '请输入技能名称' }]}
+              >
+                <Select placeholder="请选择或输入技能" mode="tags">
+                  {skillCategories.map(category => (
+                    <Option key={category} value={category}>{category}</Option>
+                  ))}
+                </Select>
+              </Form.Item>
+            ) : (
+              <>
+                <Form.Item
+                  name="name"
+                  label="证书名称"
+                  rules={[{ required: true, message: '请输入证书名称' }]}
+                >
+                  <Input placeholder="请输入证书名称" />
+                </Form.Item>
+                
+                <Form.Item
+                  name="issuingAgency"
+                  label="颁发机构"
+                  rules={[{ required: true, message: '请输入颁发机构' }]}
+                >
+                  <Input placeholder="请输入颁发机构" />
+                </Form.Item>
+                
+                <Form.Item
+                  name="validityPeriod"
+                  label="有效期"
+                  rules={[{ required: true, message: '请选择有效期' }]}
+                >
+                  <Input placeholder="例:2025-12-31" />
+                </Form.Item>
+                
+                <Form.Item name="description" label="证书描述">
+                  <Input.TextArea 
+                    rows={3} 
+                    placeholder="请输入证书描述" 
+                    maxLength={200}
+                  />
+                </Form.Item>
+                
+                <Form.Item name="imageUrl" label="证书图片">
+                  <Upload
+                    customRequest={handleUploadCertificate}
+                    showUploadList={false}
+                    accept="image/*"
+                  >
+                    <Button icon={<UploadOutlined />}>上传证书图片</Button>
+                  </Upload>
+                </Form.Item>
+              </>
+            )}
+          </Form>
+        </Modal>
+      </div>
+    </div>
+  );
+};
+
+export default SkillsPage;

+ 45 - 0
src/client/mobile/routes.tsx

@@ -17,6 +17,11 @@ import TimeBankPage from './pages/TimeBankPage';
 import PolicyNewsPage from './pages/PolicyNewsPage';
 import PublishPage from './pages/PublishPage';
 import ProfilePage from './pages/ProfilePage';
+import ProfileEditPage from './pages/ProfileEditPage';
+import PointsPage from './pages/PointsPage';
+import MyPostsPage from './pages/MyPostsPage';
+import MyFavoritesPage from './pages/MyFavoritesPage';
+import SkillsPage from './pages/SkillsPage';
 import LoginPage from './pages/LoginPage';
 import RegisterPage from './pages/RegisterPage';
 
@@ -72,6 +77,46 @@ export const router = createBrowserRouter([
             <ProfilePage />
           </ProtectedRoute>
         )
+      },
+      {
+        path: 'profile/edit',
+        element: (
+          <ProtectedRoute>
+            <ProfileEditPage />
+          </ProtectedRoute>
+        )
+      },
+      {
+        path: 'profile/points',
+        element: (
+          <ProtectedRoute>
+            <PointsPage />
+          </ProtectedRoute>
+        )
+      },
+      {
+        path: 'profile/posts',
+        element: (
+          <ProtectedRoute>
+            <MyPostsPage />
+          </ProtectedRoute>
+        )
+      },
+      {
+        path: 'profile/favorites',
+        element: (
+          <ProtectedRoute>
+            <MyFavoritesPage />
+          </ProtectedRoute>
+        )
+      },
+      {
+        path: 'profile/skills',
+        element: (
+          <ProtectedRoute>
+            <SkillsPage />
+          </ProtectedRoute>
+        )
       }
     ]
   },

+ 477 - 0
银龄平台移动端及平台端-完善版.md

@@ -0,0 +1,477 @@
+# 银龄智慧平台完整设计方案
+## (移动端小程序 + PC管理后台 + 技术架构)
+
+### 一、项目概述
+
+银龄智慧平台是一个面向老年群体的数字化服务平台,通过"互联网+养老"模式,整合企业招聘、个人求职、知识分享、时间银行、积分兑换等资源,构建银龄群体与企业的精准匹配生态。
+
+### 二、技术架构设计
+
+#### 2.1 系统架构图
+
+```mermaid
+graph TB
+    subgraph 前端层
+        A[移动端小程序] -->|调用| API[REST API]
+        B[PC管理后台] -->|调用| API
+    end
+    
+    subgraph 服务层
+        API -->|路由分发| Controller
+        Controller -->|业务逻辑| Service
+        Service -->|数据访问| Repository
+    end
+    
+    subgraph 数据层
+        Repository -->|ORM| MySQL[(MySQL数据库)]
+        Repository -->|缓存| Redis[(Redis缓存)]
+    end
+    
+    subgraph 存储层
+        MySQL -->|文件存储| MinIO[MinIO对象存储]
+    end
+```
+
+#### 2.2 技术栈
+
+**前端技术栈:**
+- 移动端:React Native + TypeScript + NativeBase
+- 管理后台:React + TypeScript + Ant Design Pro
+- 状态管理:Zustand
+- 网络请求:Axios + React Query
+
+**后端技术栈:**
+- 框架:Hono.js + TypeORM
+- 数据库:MySQL 8.0
+- 缓存:Redis 6.0
+- 文件存储:MinIO
+- 部署:Docker容器化
+
+#### 2.3 实体架构总览
+
+平台共包含23个实体,按业务模块聚合:
+
+| 模块 | 实体数量 | 核心业务 |
+|------|----------|----------|
+| 用户系统 | 3个 | 用户管理、认证授权 |
+| 银龄用户系统 | 15个 | 知识分享、时间银行、积分体系 |
+| 招聘系统 | 7个 | 企业招聘、求职申请 |
+| 文件系统 | 1个 | 文件管理、图片存储 |
+
+### 三、移动端功能设计(小程序端)
+
+#### 3.1 功能架构图
+
+```mermaid
+graph TD
+    A[首页] --> B[服务分类]
+    A --> C[推荐服务]
+    A --> D[搜索功能]
+    
+    B --> B1[银龄岗]
+    B --> B2[银龄库]
+    B --> B3[银龄智库]
+    B --> B4[时间银行]
+    B --> B5[老年大学]
+    B --> B6[政策资讯]
+    
+    E[底部导航] --> F[首页]
+    E --> G[银龄岗]
+    E --> H[发布中心]
+    E --> I[银龄库]
+    E --> J[个人中心]
+```
+
+#### 3.2 功能模块详细设计
+
+##### 3.2.1 首页模块
+- **顶部搜索区域**
+  - 全局搜索:支持岗位、人才、知识、政策多维度搜索
+  - 智能联想:基于用户历史行为推荐关键词
+  - 语音搜索:适老化设计,支持语音输入
+
+- **轮播Banner区域**
+  - 运营位管理:后台可配置6张轮播图
+  - 跳转逻辑:支持跳转到具体服务页面或外部链接
+  - 展示策略:基于用户画像的个性化展示
+
+- **服务分类导航**
+  - 银龄岗:企业招聘岗位展示
+  - 银龄库:银龄人才库
+  - 银龄智库:知识分享平台
+  - 时间银行:志愿服务积分系统
+  - 老年大学:教育资源聚合
+  - 政策资讯:最新政策动态
+
+##### 3.2.2 银龄岗(招聘模块)
+- **岗位列表**
+  - 筛选条件:职位类型、工作经验、薪资范围、工作地区
+  - 排序方式:智能推荐、发布时间、薪资高低、距离优先
+  - 卡片展示:企业logo、职位名称、薪资范围、工作地点、发布时间
+
+- **岗位详情**
+  - 企业信息:企业简介、规模、认证状态
+  - 职位信息:详细描述、任职要求、福利待遇
+  - 互动功能:收藏、分享、立即申请
+  - 相似推荐:基于技能匹配的岗位推荐
+
+- **申请流程**
+  - 简历匹配:自动匹配用户简历与岗位要求
+  - 在线沟通:内置IM系统支持与企业HR沟通
+  - 申请状态:实时跟踪申请进度
+
+##### 3.2.3 银龄库(人才库模块)
+- **人才展示**
+  - 个人卡片:头像、姓名、年龄、认证标识、积分等级
+  - 技能标签:可视化展示专业技能标签
+  - 工作经历:时间轴形式展示工作履历
+
+- **人才详情**
+  - 基础信息:联系方式、期望薪资、工作地点偏好
+  - 技能证书:展示相关技能认证证书
+  - 项目经验:详细的项目经历描述
+  - 评价系统:企业服务后的互评机制
+
+- **智能匹配**
+  - 岗位匹配度:基于技能标签的岗位匹配算法
+  - 推荐优先级:积分等级、活跃度、认证状态权重计算
+
+##### 3.2.4 银龄智库(知识分享)
+- **知识分类**
+  - 一级分类:医疗健康、农业技术、教育培训、生活技能
+  - 二级分类:每个大类下的细分标签
+  - 热门标签:基于使用频率的动态标签展示
+
+- **内容形式**
+  - 图文混排:支持富文本编辑器的内容发布
+  - 视频教程:支持本地视频上传和在线播放
+  - 文档下载:PDF、Word等格式的知识文档
+
+- **互动功能**
+  - 点赞收藏:用户对内容的互动行为
+  - 评论讨论:知识内容的交流讨论区
+  - 积分激励:发布知识、被下载获得积分奖励
+
+##### 3.2.5 时间银行(志愿服务)
+- **服务记录**
+  - 服务类型:志愿服务、技能培训、咨询指导
+  - 时间记录:精确到小时的服务时长统计
+  - 积分兑换:1小时服务=10积分的兑换规则
+
+- **案例展示**
+  - 滚动案例:优秀服务案例的轮播展示
+  - 数据统计:累计服务人员、总积分、参与人数
+  - 时间轴:个人服务经历的可视化展示
+
+- **积分体系**
+  - 获取途径:服务他人、知识分享、平台贡献
+  - 使用场景:商品兑换、服务购买、公益捐赠
+  - 等级体系:白银、黄金、铂金、钻石会员等级
+
+##### 3.2.6 个人中心
+- **信息管理**
+  - 基础资料:个人信息编辑、头像上传
+  - 认证中心:身份认证、技能认证、企业认证
+  - 隐私设置:信息可见性控制、联系方式保护
+
+- **我的技能**
+  - 技能标签:可自定义添加的技能标签
+  - 证书上传:技能认证证书的图片上传
+  - 技能匹配:技能标签与岗位需求的匹配度
+
+- **积分管理**
+  - 积分余额:实时积分余额展示
+  - 积分流水:详细的积分获取和消费记录
+  - 兑换中心:商品兑换、服务预约、公益捐赠
+
+- **个性化设置**
+  - 字体大小:小、中、大、超大四档调节
+  - 深色模式:适老化的夜间模式
+  - 语音播报:重要信息的语音朗读功能
+
+### 四、PC管理后台设计
+
+#### 4.1 管理后台架构图
+
+```mermaid
+graph TD
+    subgraph 后台管理模块
+        A[系统管理] --> A1[基础配置]
+        A --> A2[权限管理]
+        
+        B[用户管理] --> B1[个人用户]
+        B --> B2[企业用户]
+        
+        C[内容管理] --> C1[轮播图管理]
+        C --> C2[分类管理]
+        C --> C3[政策资讯]
+        C --> C4[老年大学]
+        
+        D[业务管理] --> D1[银龄岗管理]
+        D --> D2[银龄库管理]
+        D --> D3[银龄智库管理]
+        
+        E[积分管理] --> E1[时间银行]
+        E --> E2[积分体系]
+        E --> E3[兑换管理]
+        
+        F[数据中心] --> F1[用户统计]
+        F --> F2[业务统计]
+        F --> F3[运营分析]
+    end
+```
+
+#### 4.2 核心管理模块
+
+##### 4.2.1 系统管理模块
+- **基础配置管理**
+  - 平台信息:名称、Logo、版权信息配置
+  - 移动端配置:底部导航文案、图标自定义
+  - 全局参数:搜索默认条件、推荐算法阈值
+  - 轮播图管理:6张轮播图的上传、排序、生效时间设置
+
+- **权限体系**
+  - 角色管理:超级管理员、审核员、运营员、商户专员
+  - 权限矩阵:基于RBAC的细粒度权限控制
+  - 操作日志:完整的操作审计追踪
+  - 账号安全:登录IP限制、密码策略、二步验证
+
+##### 4.2.2 用户管理模块
+- **个人用户管理**
+  - 用户列表:基础信息展示、多维度筛选
+  - 用户档案:完整的信息查看和编辑
+  - 认证审核:身份认证、技能认证的材料审核
+  - 批量操作:批量认证、批量标签管理
+
+- **企业用户管理**
+  - 企业档案:企业信息、认证材料、岗位发布统计
+  - 认证流程:营业执照、资质证书的上传和审核
+  - 账号状态:启用、禁用、限制发布等状态管理
+  - 信用体系:基于用户评价的信用评分
+
+##### 4.2.3 内容管理模块
+- **知识内容管理**
+  - 分类体系:支持多级分类的动态配置
+  - 内容审核:用户提交内容的审核流程
+  - 标签管理:知识标签的创建、编辑、关联
+  - 推荐算法:基于内容质量和互动数据的推荐权重
+
+- **政策资讯管理**
+  - 资讯发布:富文本编辑器、图片视频上传
+  - 分类管理:政策类型、适用人群的标签分类
+  - 阅读量统计:实时阅读量、分享数据追踪
+  - 推送机制:重要资讯的APP推送和短信通知
+
+##### 4.2.4 业务管理模块
+- **招聘管理**
+  - 岗位审核:发布内容的合法性审核
+  - 标签标准化:统一的岗位技能标签库
+  - 匹配算法:岗位与个人技能的匹配度计算
+  - 违规处理:虚假信息的下架和处罚机制
+
+- **积分体系管理**
+  - 规则配置:积分获取、消费的标准化规则
+  - 等级体系:白银、黄金、铂金、钻石的等级设置
+  - 兑换管理:商品录入、库存管理、兑换记录
+  - 公益积分:捐赠记录、使用明细的完整追踪
+
+##### 4.2.5 数据中心模块
+- **实时数据看板**
+  - 用户统计:总用户数、日活、月活、留存率
+  - 业务统计:岗位发布、求职申请、知识分享、积分流转
+  - 运营指标:各模块访问量、转化率、用户满意度
+
+- **数据分析报表**
+  - 用户画像:年龄分布、技能分布、地域分布
+  - 行为分析:用户路径、停留时长、功能使用频率
+  - 业务分析:岗位匹配成功率、知识分享活跃度
+
+### 五、数据架构与实体关系
+
+#### 5.1 核心实体关系图
+
+```mermaid
+erDiagram
+    UserEntity ||--o{ SilverUserProfile : has
+    UserEntity ||--o{ UserPreference : has
+    UserEntity ||--o{ SilverTimeBank : records
+    UserEntity ||--o{ SilverKnowledge : shares
+    UserEntity ||--o{ Application : applies
+    UserEntity ||--o{ Favorite : favorites
+    
+    Company ||--o{ Job : posts
+    Company ||--o{ CompanyImage : has
+    Job ||--o{ Application : receives
+    Job ||--o{ Favorite : receives
+    Job ||--o{ ViewRecord : tracks
+    
+    SilverKnowledge ||--o{ SilverKnowledgeTagRelation : tagged
+    SilverKnowledge }o--|| SilverKnowledgeCategory : belongs
+    SilverKnowledgeTag ||--o{ SilverKnowledgeTagRelation : tags
+```
+
+#### 5.2 数据流转设计
+
+**用户注册流程:**
+1. 基础用户创建(UserEntity)
+2. 银龄档案完善(SilverUserProfile)
+3. 偏好设置初始化(UserPreference)
+4. 积分账户创建(SilverPoint)
+
+**知识分享流程:**
+1. 内容创建(SilverKnowledge)
+2. 分类关联(SilverKnowledgeCategory)
+3. 标签绑定(SilverKnowledgeTagRelation)
+4. 统计记录更新(SilverKnowledgeStats)
+5. 用户积分奖励(SilverPointTransaction)
+
+**求职申请流程:**
+1. 岗位浏览(ViewRecord)
+2. 岗位收藏(Favorite)
+3. 简历投递(Application)
+4. 状态跟踪(Application.status)
+5. 双方互评机制
+
+### 六、安全与性能设计
+
+#### 6.1 安全架构
+
+**认证授权:**
+- JWT Token认证机制
+- 基于角色的权限控制(RBAC)
+- 敏感数据字段级加密
+- 操作日志完整审计
+
+**数据安全:**
+- 个人信息脱敏展示
+- 联系方式隐私保护
+- 文件上传安全检测
+- 接口防刷和限流机制
+
+#### 6.2 性能优化
+
+**数据库优化:**
+- 读写分离架构
+- 关键字段索引优化
+- 分页查询深度优化
+- 缓存策略(Redis)
+
+**前端优化:**
+- 图片懒加载和压缩
+- 接口数据分页加载
+- 本地缓存策略
+- CDN加速部署
+
+**系统监控:**
+- 接口响应时间监控
+- 数据库性能监控
+- 用户行为分析
+- 异常报警机制
+
+### 七、部署与运维方案
+
+#### 7.1 容器化部署
+
+```yaml
+# docker-compose.yml
+version: '3.8'
+services:
+  app:
+    build: .
+    ports:
+      - "3000:3000"
+    environment:
+      - NODE_ENV=production
+    depends_on:
+      - mysql
+      - redis
+      
+  mysql:
+    image: mysql:8.0
+    environment:
+      - MYSQL_ROOT_PASSWORD=password
+      - MYSQL_DATABASE=silver_platform
+      
+  redis:
+    image: redis:6.0-alpine
+    
+  minio:
+    image: minio/minio
+    command: server /data --console-address ":9001"
+```
+
+#### 7.2 环境配置
+
+**开发环境:**
+- 本地MySQL + Redis
+- 开发工具:VSCode + Docker
+- 调试工具:React DevTools + Postman
+
+**测试环境:**
+- 独立测试数据库
+- 自动化测试流程
+- 性能压力测试
+
+**生产环境:**
+- 阿里云ECS部署
+- 负载均衡配置
+- 数据库主从架构
+- CDN加速配置
+
+### 八、扩展性设计
+
+#### 8.1 接口扩展
+- RESTful API设计规范
+- 版本控制(/api/v1/, /api/v2/)
+- 插件化架构支持
+- 第三方服务集成预留
+
+#### 8.2 功能扩展
+- AI智能匹配算法
+- 视频面试功能
+- 在线培训课程
+- 社区互动功能
+
+#### 8.3 数据扩展
+- 支持多维度标签体系
+- 弹性字段扩展
+- 数据仓库建设
+- BI分析报表
+
+### 九、项目实施计划
+
+#### 9.1 开发阶段
+1. **第一阶段(4周):** 基础架构搭建、用户系统、认证授权
+2. **第二阶段(6周):** 核心业务功能开发(招聘、知识、积分)
+3. **第三阶段(4周):** 管理后台、数据分析、性能优化
+4. **第四阶段(2周):** 测试、部署、上线准备
+
+#### 9.2 测试策略
+- 单元测试:核心业务逻辑
+- 集成测试:接口联调测试
+- 性能测试:并发用户压力测试
+- 安全测试:渗透测试和安全扫描
+
+### 十、运营与维护
+
+#### 10.1 内容运营
+- 知识内容审核机制
+- 用户生成内容监管
+- 热门内容推荐策略
+- 违规内容处理流程
+
+#### 10.2 用户运营
+- 新用户引导流程
+- 用户活跃度提升
+- 积分激励体系优化
+- 用户反馈收集机制
+
+#### 10.3 技术运维
+- 7x24小时监控
+- 故障应急响应
+- 数据备份策略
+- 版本迭代管理
+
+---
+
+**项目总结:** 银龄智慧平台通过完整的技术架构设计、丰富的功能模块、严密的安全保障和可扩展的架构设计,为老年群体提供了一站式的数字化服务解决方案,实现了"老有所为、老有所学、老有所乐"的平台愿景。

+ 1 - 1
银龄平台移动端及平台端.md

@@ -1,4 +1,4 @@
-银龄智慧平台功能设计方案(小程序端 + 后台)
+银龄智慧平台移动端及平台端(小程序端 + 后台)
 一、小程序端(前端)功能设计
 (一)顶部区域:查询  搜索  移动端 
 (二)滚动band区域:混动轮播