Jelajahi Sumber

✨ feat(mobile): implement bottom tab navigation system

- add BottomTabBar component with customizable tabs and active state management
- create MobileLayout component integrating main content area and bottom tab bar
- refactor ElderlyCarePage to remove inline footer and use new layout structure
- add tab configurations for default mobile and elderly care scenarios
- update routing system to support tab-based navigation structure

✨ feat(routes): add mobile tab navigation routes

- add mobile route group with tab-based navigation structure
- configure elderly care specific tab navigation
- add placeholder pages for discover, favorites and profile tabs
- update root route to redirect to elderly care page

♻️ refactor(elderly-care): optimize page structure for layout integration

- remove inline footer navigation from ElderlyCarePage
- adjust main content container to work with MobileLayout
- maintain all existing page content and functionality while adapting to new layout
- ensure proper spacing and scrolling behavior with bottom tab bar
yourname 8 bulan lalu
induk
melakukan
b5fc265eda

+ 79 - 0
src/client/mobile/components/BottomTabBar.tsx

@@ -0,0 +1,79 @@
+import React from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+
+export interface TabItem {
+  id: string;
+  name: string;
+  icon: string;
+  path: string;
+  active?: boolean;
+}
+
+interface BottomTabBarProps {
+  tabs: TabItem[];
+  onTabChange?: (tab: TabItem) => void;
+}
+
+export const BottomTabBar: React.FC<BottomTabBarProps> = ({ 
+  tabs, 
+  onTabChange 
+}) => {
+  const location = useLocation();
+  const navigate = useNavigate();
+
+  const handleTabClick = (tab: TabItem) => {
+    navigate(tab.path);
+    onTabChange?.(tab);
+  };
+
+  return (
+    <footer className="bg-white border-t border-gray-200 py-2">
+      <div className="grid grid-cols-5 gap-2 text-center">
+        {tabs.map((tab) => {
+          const isActive = location.pathname === tab.path || (tab.active && location.pathname.startsWith(tab.path));
+          
+          return (
+            <button
+              key={tab.id}
+              onClick={() => handleTabClick(tab)}
+              className={`flex flex-col items-center py-1 transition-colors ${
+                isActive ? 'text-orange-500' : 'text-gray-500'
+              }`}
+            >
+              {tab.id === 'add' ? (
+                <div className="bg-orange-500 text-white w-12 h-12 rounded-full flex items-center justify-center text-xl -mt-4 shadow-lg">
+                  {tab.icon}
+                </div>
+              ) : (
+                <>
+                  <span className="text-xl mb-1">{tab.icon}</span>
+                  <span className={`text-xs ${isActive ? 'font-medium' : ''}`}>
+                    {tab.name}
+                  </span>
+                </>
+              )}
+            </button>
+          );
+        })}
+      </div>
+    </footer>
+  );
+};
+
+// 默认的移动端标签配置
+export const defaultMobileTabs: TabItem[] = [
+  { id: 'home', name: '首页', icon: '🏠', path: '/home' },
+  { id: 'discover', name: '发现', icon: '🔍', path: '/discover' },
+  { id: 'add', name: '添加', icon: '➕', path: '/add' },
+  { id: 'favorites', name: '收藏', icon: '❤️', path: '/favorites' },
+  { id: 'profile', name: '我的', icon: '👤', path: '/profile' }
+];
+
+// 养老服务专用的标签配置
+export const elderlyCareTabs: TabItem[] = [
+  { id: 'elderly-care', name: '首页', icon: '🏠', path: '/elderly-care', active: true },
+  { id: 'discover', name: '发现', icon: '🔍', path: '/discover' },
+  { id: 'add', name: '添加', icon: '➕', path: '/add' },
+  { id: 'favorites', name: '收藏', icon: '❤️', path: '/favorites' },
+  { id: 'profile', name: '我的', icon: '👤', path: '/profile' }
+];

+ 33 - 0
src/client/mobile/layouts/MobileLayout.tsx

@@ -0,0 +1,33 @@
+import React from 'react';
+import { Outlet } from 'react-router-dom';
+import { BottomTabBar, TabItem } from '../components/BottomTabBar';
+
+export interface MobileLayoutProps {
+  tabs?: TabItem[];
+  customTabs?: boolean;
+}
+
+/**
+ * 移动端一级页面布局组件
+ * 包含主内容区域和底部标签栏
+ */
+export const MobileLayout: React.FC<MobileLayoutProps> = ({ 
+  tabs = [], 
+  customTabs = false 
+}) => {
+  return (
+    <div className="min-h-screen bg-gray-50 flex flex-col">
+      {/* 主内容区域,底部留出标签栏空间 */}
+      <main className="flex-1 overflow-y-auto pb-16">
+        <Outlet />
+      </main>
+      
+      {/* 底部标签栏 */}
+      <div className="fixed bottom-0 left-0 right-0">
+        <BottomTabBar tabs={tabs} />
+      </div>
+    </div>
+  );
+};
+
+export default MobileLayout;

+ 95 - 124
src/client/mobile/pages/ElderlyCarePage.tsx

@@ -35,7 +35,7 @@ const ElderlyCarePage: React.FC = () => {
   ];
 
   return (
-    <div className="min-h-screen bg-gray-50 flex flex-col">
+    <div>
       {/* 顶部导航栏 */}
       <header className="bg-orange-500 text-white shadow-md">
         <div className="container mx-auto px-4 py-3 flex justify-between items-center">
@@ -84,146 +84,117 @@ const ElderlyCarePage: React.FC = () => {
         </div>
       </header>
 
-      <main className="flex-1 overflow-y-auto">
-        {/* 紧急救助横幅 - 2:1宽高比自适应屏幕 */}
-        <div className="bg-gradient-to-r from-orange-500 to-orange-600 text-white rounded-xl mx-4 my-4 overflow-hidden shadow-lg relative w-full" style={{ paddingTop: '50%' }}>
-          <div className="absolute inset-0 flex flex-col md:flex-row items-center p-6">
-            <div className="md:w-2/3 mb-4 md:mb-0 z-10">
-              <h2 className="text-2xl font-bold mb-2">7*24小时 紧急救助</h2>
-              <p className="text-lg mb-4">一键按下 · 十分必达</p>
-              <button className="bg-white text-orange-600 font-medium py-2 px-6 rounded-full hover:bg-gray-100 transition-colors shadow-md">
-                点击一键紧急呼救
-              </button>
-            </div>
-            <div className="md:w-1/3 flex justify-center z-10">
-              <img
-                src="https://picsum.photos/id/239/200/200"
-                alt="紧急救助"
-                className="w-32 h-32 object-cover rounded-full border-4 border-white shadow-lg"
-              />
-            </div>
+      {/* 紧急救助横幅 - 2:1宽高比自适应屏幕 */}
+      <div className="bg-gradient-to-r from-orange-500 to-orange-600 text-white rounded-xl mx-4 my-4 overflow-hidden shadow-lg relative w-full" style={{ paddingTop: '50%' }}>
+        <div className="absolute inset-0 flex flex-col md:flex-row items-center p-6">
+          <div className="md:w-2/3 mb-4 md:mb-0 z-10">
+            <h2 className="text-2xl font-bold mb-2">7*24小时 紧急救助</h2>
+            <p className="text-lg mb-4">一键按下 · 十分必达</p>
+            <button className="bg-white text-orange-600 font-medium py-2 px-6 rounded-full hover:bg-gray-100 transition-colors shadow-md">
+              点击一键紧急呼救
+            </button>
+          </div>
+          <div className="md:w-1/3 flex justify-center z-10">
+            <img
+              src="https://picsum.photos/id/239/200/200"
+              alt="紧急救助"
+              className="w-32 h-32 object-cover rounded-full border-4 border-white shadow-lg"
+            />
           </div>
         </div>
+      </div>
 
-        {/* 服务分类图标区域 */}
-        <div className="grid grid-cols-5 gap-3 px-4 mb-6">
-          {serviceCategories.map(category => (
-            <div key={category.id} className="flex flex-col items-center">
-              <div className={`${category.color} text-white w-14 h-14 rounded-full flex items-center justify-center text-xl mb-2 shadow-md`}>
-                {category.icon}
-              </div>
-              <span className="text-xs text-gray-700 text-center">{category.name}</span>
+      {/* 服务分类图标区域 */}
+      <div className="grid grid-cols-5 gap-3 px-4 mb-6">
+        {serviceCategories.map(category => (
+          <div key={category.id} className="flex flex-col items-center">
+            <div className={`${category.color} text-white w-14 h-14 rounded-full flex items-center justify-center text-xl mb-2 shadow-md`}>
+              {category.icon}
             </div>
-          ))}
-        </div>
+            <span className="text-xs text-gray-700 text-center">{category.name}</span>
+          </div>
+        ))}
+      </div>
 
-        {/* 在线预约和电话咨询卡片 */}
-        <div className="grid grid-cols-2 gap-4 px-4 mb-6">
-          <div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100">
-            <div className="flex items-start">
-              <div className="bg-blue-100 p-3 rounded-lg mr-3">
-                <span className="text-blue-500 text-xl">📅</span>
-              </div>
-              <div>
-                <h3 className="font-bold text-gray-800 mb-1">在线预约</h3>
-                <p className="text-xs text-gray-500 mb-2">居家服务 一键预约</p>
-                <button className="text-blue-500 text-xs font-medium">立即预约 →</button>
-              </div>
+      {/* 在线预约和电话咨询卡片 */}
+      <div className="grid grid-cols-2 gap-4 px-4 mb-6">
+        <div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100">
+          <div className="flex items-start">
+            <div className="bg-blue-100 p-3 rounded-lg mr-3">
+              <span className="text-blue-500 text-xl">📅</span>
+            </div>
+            <div>
+              <h3 className="font-bold text-gray-800 mb-1">在线预约</h3>
+              <p className="text-xs text-gray-500 mb-2">居家服务 一键预约</p>
+              <button className="text-blue-500 text-xs font-medium">立即预约 →</button>
             </div>
           </div>
-          
-          <div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100">
-            <div className="flex items-start">
-              <div className="bg-green-100 p-3 rounded-lg mr-3">
-                <span className="text-green-500 text-xl">📞</span>
-              </div>
-              <div>
-                <h3 className="font-bold text-gray-800 mb-1">电话咨询</h3>
-                <p className="text-xs text-gray-500 mb-2">居家服务 电话定制</p>
-                <button className="text-green-500 text-xs font-medium">立即咨询 →</button>
-              </div>
+        </div>
+        
+        <div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100">
+          <div className="flex items-start">
+            <div className="bg-green-100 p-3 rounded-lg mr-3">
+              <span className="text-green-500 text-xl">📞</span>
+            </div>
+            <div>
+              <h3 className="font-bold text-gray-800 mb-1">电话咨询</h3>
+              <p className="text-xs text-gray-500 mb-2">居家服务 电话定制</p>
+              <button className="text-green-500 text-xs font-medium">立即咨询 →</button>
             </div>
           </div>
         </div>
+      </div>
 
-        {/* 推荐服务 */}
-        <div className="px-4 mb-6">
-          <div className="flex justify-between items-center mb-4">
-            <h2 className="text-lg font-bold text-gray-800">推荐服务</h2>
-            <div className="flex space-x-2">
-              <button className="bg-orange-500 text-white text-xs px-3 py-1 rounded-full">智能模式</button>
-              <button className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full">距离最近</button>
-              <button className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full">价格最优</button>
-            </div>
+      {/* 推荐服务 */}
+      <div className="px-4 mb-6">
+        <div className="flex justify-between items-center mb-4">
+          <h2 className="text-lg font-bold text-gray-800">推荐服务</h2>
+          <div className="flex space-x-2">
+            <button className="bg-orange-500 text-white text-xs px-3 py-1 rounded-full">智能模式</button>
+            <button className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full">距离最近</button>
+            <button className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full">价格最优</button>
           </div>
-          
-          <div className="space-y-4">
-            {recommendedServices.map(service => (
-              <div key={service.id} className="bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100">
-                <div className="flex">
-                  <img 
-                    src={service.image} 
-                    alt={service.title} 
-                    className="w-24 h-24 object-cover"
-                  />
-                  <div className="flex-1 p-4">
-                    <h3 className="font-bold text-gray-800 mb-1">{service.title}</h3>
-                    <p className="text-orange-500 font-bold mb-1">{service.price}</p>
-                    <div className="flex items-center text-xs text-gray-500 mb-2">
-                      <span className="flex items-center mr-3">
-                        <svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
-                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
-                        </svg>
-                        {service.distance}
-                      </span>
-                      <span>{service.provider}</span>
-                    </div>
-                    <div className="flex justify-end space-x-2">
-                      <button className="text-gray-500 text-xs border border-gray-200 px-3 py-1 rounded-full">
-                        <svg className="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
-                        </svg>
-                        收藏
-                      </button>
-                      <button className="bg-orange-500 text-white text-xs px-3 py-1 rounded-full">
-                        电话咨询
-                      </button>
-                    </div>
+        </div>
+        
+        <div className="space-y-4">
+          {recommendedServices.map(service => (
+            <div key={service.id} className="bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100">
+              <div className="flex">
+                <img 
+                  src={service.image} 
+                  alt={service.title} 
+                  className="w-24 h-24 object-cover"
+                />
+                <div className="flex-1 p-4">
+                  <h3 className="font-bold text-gray-800 mb-1">{service.title}</h3>
+                  <p className="text-orange-500 font-bold mb-1">{service.price}</p>
+                  <div className="flex items-center text-xs text-gray-500 mb-2">
+                    <span className="flex items-center mr-3">
+                      <svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
+                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
+                      </svg>
+                      {service.distance}
+                    </span>
+                    <span>{service.provider}</span>
+                  </div>
+                  <div className="flex justify-end space-x-2">
+                    <button className="text-gray-500 text-xs border border-gray-200 px-3 py-1 rounded-full">
+                      <svg className="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
+                      </svg>
+                      收藏
+                    </button>
+                    <button className="bg-orange-500 text-white text-xs px-3 py-1 rounded-full">
+                      电话咨询
+                    </button>
                   </div>
                 </div>
               </div>
-            ))}
-          </div>
-        </div>
-      </main>
-
-      {/* 底部导航栏 */}
-      <footer className="bg-white border-t border-gray-200 py-2">
-        <div className="grid grid-cols-5 gap-2 text-center">
-          <button className="flex flex-col items-center py-1 text-orange-500">
-            <span className="text-xl mb-1">🏠</span>
-            <span className="text-xs font-medium">首页</span>
-          </button>
-          <button className="flex flex-col items-center py-1 text-gray-500">
-            <span className="text-xl mb-1">🔍</span>
-            <span className="text-xs">发现</span>
-          </button>
-          <button className="flex flex-col items-center py-1">
-            <div className="bg-orange-500 text-white w-12 h-12 rounded-full flex items-center justify-center text-xl -mt-4 shadow-lg">
-              ➕
             </div>
-          </button>
-          <button className="flex flex-col items-center py-1 text-gray-500">
-            <span className="text-xl mb-1">❤️</span>
-            <span className="text-xs">收藏</span>
-          </button>
-          <button className="flex flex-col items-center py-1 text-gray-500">
-            <span className="text-xl mb-1">👤</span>
-            <span className="text-xs">我的</span>
-          </button>
+          ))}
         </div>
-      </footer>
+      </div>
     </div>
   );
 };

+ 34 - 1
src/client/mobile/routes.tsx

@@ -1,21 +1,54 @@
 import React from 'react';
 import { UserIcon } from '@heroicons/react/24/outline';
-import { createBrowserRouter, Navigate } from 'react-router-dom';
+import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom';
 import { ProtectedRoute } from './components/ProtectedRoute';
 import { ErrorPage } from './components/ErrorPage';
 import { NotFoundPage } from './components/NotFoundPage';
 import HomePage from './pages/HomePage';
 import ElderlyCarePage from './pages/ElderlyCarePage';
 import { MainLayout } from './layouts/MainLayout';
+import { MobileLayout } from './layouts/MobileLayout';
+import { elderlyCareTabs } from './components/BottomTabBar';
 import LoginPage from './pages/LoginPage';
 import RegisterPage from './pages/RegisterPage';
 import MemberPage from './pages/MemberPage';
+import DashboardPage from './pages/DashboardPage';
 
 export const router = createBrowserRouter([
   {
     path: '/',
     element: <Navigate to="/elderly-care" replace />
   },
+  {
+    path: '/mobile',
+    element: <MobileLayout tabs={elderlyCareTabs} />,
+    children: [
+      {
+        path: 'elderly-care',
+        element: <ElderlyCarePage />
+      },
+      {
+        path: 'home',
+        element: <HomePage />
+      },
+      {
+        path: 'dashboard',
+        element: <DashboardPage />
+      },
+      {
+        path: 'discover',
+        element: <div className="p-4 text-center">发现页面开发中...</div>
+      },
+      {
+        path: 'favorites',
+        element: <div className="p-4 text-center">收藏页面开发中...</div>
+      },
+      {
+        path: 'profile',
+        element: <div className="p-4 text-center">个人中心页面开发中...</div>
+      }
+    ]
+  },
   {
     path: '/elderly-care',
     element: <ElderlyCarePage />