소스 검색

Merge remote-tracking branch 'mini-starter/mini-multi-tenant-mall' into 租户2

yourname 2 주 전
부모
커밋
595676ec6b

+ 4 - 4
mini/src/components/ui/city-selector.tsx

@@ -15,10 +15,10 @@ interface CitySelectorProps {
   cityValue?: number
   districtValue?: number
   townValue?: number
-  onProvinceChange?: (value: number) => void
-  onCityChange?: (value: number) => void
-  onDistrictChange?: (value: number) => void
-  onTownChange?: (value: number) => void
+  onProvinceChange?: (value: number | undefined) => void
+  onCityChange?: (value: number | undefined) => void
+  onDistrictChange?: (value: number | undefined) => void
+  onTownChange?: (value: number | undefined) => void
   disabled?: boolean
   showLabels?: boolean
 }

+ 199 - 90
mini/src/pages/index/index.tsx

@@ -5,7 +5,7 @@ import { TabBarLayout } from '@/layouts/tab-bar-layout'
 import TDesignSearch from '@/components/tdesign/search'
 import GoodsList from '@/components/goods-list'
 import { GoodsData } from '@/components/goods-card'
-import { goodsClient, advertisementClient } from '@/api'
+import { goodsClient, advertisementClient, authClient } from '@/api'
 import './index.css'
 import { useAuth } from '@/utils/auth'
 import { useCart } from '@/contexts/CartContext'
@@ -56,9 +56,11 @@ interface Goods {
 }
 
 const HomePage: React.FC = () => {
-  const { isLoggedIn } = useAuth();
+  const { isLoggedIn, setUser } = useAuth();
   const { addToCart } = useCart();
   const [refreshing, setRefreshing] = React.useState(false);
+  const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
+  const [isLoggingIn, setIsLoggingIn] = React.useState(false);
   
   // 广告数据查询
   const {
@@ -242,8 +244,59 @@ const HomePage: React.FC = () => {
     }
   })
 
-  // 如果未登录,不渲染页面内容
-  if (!isLoggedIn) return null
+  // 未登录时显示隐私政策弹框
+  React.useEffect(() => {
+    if (!isLoggedIn) {
+      setShowPrivacyModal(true)
+    }
+  }, [isLoggedIn])
+
+  // 同意隐私政策,直接静默登录获取openid
+  const handleAgreePrivacy = async () => {
+    setIsLoggingIn(true)
+    try {
+      // 获取登录code
+      const loginRes = await Taro.login()
+      if (!loginRes.code) {
+        throw new Error('获取登录凭证失败')
+      }
+
+      // 调用后端静默登录API(不传userInfo)
+      const response = await authClient['mini-login'].$post({
+        json: {
+          code: loginRes.code,
+          tenantId: Number(process.env.TARO_APP_TENANT_ID) || 1
+        }
+      })
+
+      if (response.status === 200) {
+        const { token, user } = await response.json()
+
+        // 保存token和用户信息
+        Taro.setStorageSync('userInfo', user)
+        Taro.setStorageSync('mini_token', token)
+        setUser(user as any)
+
+        setShowPrivacyModal(false)
+        Taro.showToast({ title: '登录成功', icon: 'success' })
+      } else {
+        throw new Error('登录失败')
+      }
+    } catch (error: any) {
+      console.error('静默登录失败:', error)
+      // 静默登录失败,跳转到登录页
+      setShowPrivacyModal(false)
+      Taro.navigateTo({ url: '/pages/login/index' })
+    } finally {
+      setIsLoggingIn(false)
+    }
+  }
+
+  // 拒绝隐私政策,跳转到登录页
+  const handleRejectPrivacy = () => {
+    setShowPrivacyModal(false)
+    Taro.navigateTo({ url: '/pages/login/index' })
+  }
 
   // 跳转到商品详情
   const handleGoodsClick = (goods: GoodsData, index: number) => {
@@ -325,110 +378,166 @@ const HomePage: React.FC = () => {
   }
 
   return (
-    <TabBarLayout activeKey="home">
-      <Navbar
-        title="首页"
-        leftIcon=""
-        onClickLeft={() => Taro.navigateBack()}
-        rightIcon=""
-        onClickRight={() => {}}
-      />
-      <ScrollView
-        className="home-scroll-view"
-        scrollY
-        refresherEnabled={true}
-        refresherTriggered={refreshing}
-        onRefresherRefresh={handleRefresh}
-        onScrollToLower={handleScrollToLower}      >
-        {/* 页面头部 - 搜索栏和轮播图 */}
-        <View className="home-page-header">
-          {/* 搜索栏 */}
-          <View className="search" onClick={handleSearchClick}>
-            <TDesignSearch
-              placeholder="搜索商品..."
-              disabled={true}
-              shape="round"
-            />
+    <>
+      <TabBarLayout activeKey="home">
+        <Navbar
+          title="首页"
+          leftIcon=""
+          onClickLeft={() => Taro.navigateBack()}
+          rightIcon=""
+          onClickRight={() => {}}
+        />
+        <ScrollView
+          className="home-scroll-view"
+          scrollY
+          refresherEnabled={true}
+          refresherTriggered={refreshing}
+          onRefresherRefresh={handleRefresh}
+          onScrollToLower={handleScrollToLower}      >
+          {/* 页面头部 - 搜索栏和轮播图 */}
+          <View className="home-page-header">
+            {/* 搜索栏 */}
+            <View className="search" onClick={handleSearchClick}>
+              <TDesignSearch
+                placeholder="搜索商品..."
+                disabled={true}
+                shape="round"
+              />
+            </View>
+
+            {/* 轮播图 */}
+            <View className="swiper-wrap">
+              {isAdLoading ? (
+                <View className="loading-container">
+                  <Text className="loading-text">广告加载中...</Text>
+                </View>
+              ) : adError ? (
+                <View className="error-container">
+                  <Text className="error-text">广告加载失败</Text>
+                </View>
+              ) : finalImgSrcs && finalImgSrcs.length > 0 ? (
+
+                <Carousel
+                  items={finalImgSrcs.filter((item: any) => item.imageFile?.fullUrl).map((item: any) => ({
+                    src: item.imageFile!.fullUrl,
+                    title: item.title || '',
+                    description: item.description || ''
+                  }))}
+                  height={800}
+                  autoplay={true}
+                  interval={3000}
+                  circular={true}
+                  imageMode="aspectFit"
+                />
+
+              )
+              : (
+                <View className="empty-container">
+                  <Text className="empty-text">暂无广告</Text>
+                </View>
+              )}
+            </View>
           </View>
 
-          {/* 轮播图 */}
-          <View className="swiper-wrap">
-            {isAdLoading ? (
+          {/* 页面内容 - 商品列表 */}
+          <View className="home-page-container">
+            {isLoading ? (
               <View className="loading-container">
-                <Text className="loading-text">广告加载中...</Text>
+                <Text className="loading-text">加载中...</Text>
               </View>
-            ) : adError ? (
+            ) : error ? (
               <View className="error-container">
-                <Text className="error-text">广告加载失败</Text>
+                <Text className="error-text">加载失败,请重试</Text>
               </View>
-            ) : finalImgSrcs && finalImgSrcs.length > 0 ? (
-            
-              <Carousel
-                items={finalImgSrcs.filter(item => item.imageFile?.fullUrl).map(item => ({
-                  src: item.imageFile!.fullUrl,
-                  title: item.title || '',
-                  description: (item as any).description || ''
-                }))}
-                height={800}
-                autoplay={true}
-                interval={4000}
-                circular={true}
-                imageMode="aspectFit"
-              />
-            
-            )
-            : (
+            ) : goodsList.length === 0 ? (
               <View className="empty-container">
-                <Text className="empty-text">暂无广告</Text>
+                <Text className="empty-text">暂无商品</Text>
               </View>
-            )}
-          </View>
-        </View>
+            ) : (
+              <>
+                <GoodsList
+                  goodsList={goodsList}
+                  onClick={handleGoodsClick}
+                  onAddCart={handleAddCart}
+                />
 
-        {/* 页面内容 - 商品列表 */}
-        <View className="home-page-container">
-          {isLoading ? (
-            <View className="loading-container">
-              <Text className="loading-text">加载中...</Text>
-            </View>
-          ) : error ? (
-            <View className="error-container">
-              <Text className="error-text">加载失败,请重试</Text>
-            </View>
-          ) : goodsList.length === 0 ? (
-            <View className="empty-container">
-              <Text className="empty-text">暂无商品</Text>
-            </View>
-          ) : (
-            <>
-              <GoodsList
-                goodsList={goodsList}
-                onClick={handleGoodsClick}
-                onAddCart={handleAddCart}
-              />
+                {/* 加载更多状态 */}
+                {isFetchingNextPage && (
+                  <View className="loading-more-container">
+                    <Text className="loading-more-text">加载更多...</Text>
+                  </View>
+                )}
 
-              {/* 加载更多状态 */}
-              {isFetchingNextPage && (
-                <View className="loading-more-container">
-                  <Text className="loading-more-text">加载更多...</Text>
+                {/* 无更多数据状态 */}
+                {!hasNextPage && goodsList.length > 0 && (
+                  <View className="no-more-container">
+                  <Text className="no-more-text">
+                    {`已经到底啦 (共${goodsList.length}件商品)`}
+                  </Text>
                 </View>
               )}
-
-              {/* 无更多数据状态 */}
-              {!hasNextPage && goodsList.length > 0 && (
-                <View className="no-more-container">
-                <Text className="no-more-text">
-                  {`已经到底啦 (共${goodsList.length}件商品)`}
-                </Text>
-              </View>
-              )}
             </>
           )}
           <View className='height130'></View>
         </View>
-        
+
       </ScrollView>
     </TabBarLayout>
+
+      {/* 隐私政策弹框 - 模态弹框,最上层 */}
+      {showPrivacyModal && (
+        <View className="fixed inset-0 z-[9999] flex items-end justify-center" style={{backgroundColor: 'rgba(0, 0, 0, 0.5)'}}>
+          <View className="bg-white rounded-t-2xl w-full max-h-[70vh] flex flex-col">
+            {/* 标题栏 */}
+            <View className="flex justify-center items-center py-4 border-b border-gray-200 flex-shrink-0">
+              <View className="w-12 h-1 bg-gray-300 rounded-full"></View>
+            </View>
+
+            {/* 内容 */}
+            <View className="p-6 flex-1 overflow-y-auto">
+              {/* 温馨提示标题 */}
+              <View className="flex items-center justify-center mb-6">
+                <Text className="text-lg font-bold text-gray-900">温馨提示</Text>
+              </View>
+
+              {/* 隐私政策内容 */}
+              <View className="space-y-4 mb-8">
+                <Text className="text-sm text-gray-700 leading-relaxed">
+                  亲爱的用户,欢迎使用小程序。我们依据相关法律制定了小程序用户隐私政策,请您在使用我们的产品前仔细阅读并充分理解相关条款,以了解您的权利。
+                </Text>
+                <Text className="text-sm text-gray-700 leading-relaxed">
+                  感谢您的支持与关注,小程序为您提供在线下单服务等功能,在您使用相应功能时,我们会根据服务内容获取必要的个人信息,请您仔细阅读
+                  <Text className="text-blue-500">《商家小程序隐私保护声明》</Text>
+                </Text>
+                <Text className="text-sm text-gray-700 leading-relaxed">
+                  如您同意此协议,请点击同意。
+                </Text>
+              </View>
+            </View>
+
+            {/* 按钮组 - 固定在底部 */}
+            <View className="p-6 pt-0 flex-shrink-0">
+              <View className="flex space-x-3">
+                <View
+                  className="flex-1 border border-gray-300 text-gray-600 text-center py-3 rounded-lg"
+                  onClick={handleRejectPrivacy}
+                >
+                  <Text className="text-gray-600">拒绝</Text>
+                </View>
+                 <View
+                  className="flex-1 bg-green-500 text-white text-center py-3 rounded-lg"
+                  onClick={handleAgreePrivacy}
+                >
+                  <Text className="text-white font-medium">
+                    {isLoggingIn ? '登录中...' : '同意'}
+                  </Text>
+                </View>
+              </View>
+            </View>
+          </View>
+        </View>
+      )}
+    </>
   )
 }
 

+ 8 - 3
mini/src/pages/login/wechat-login.tsx

@@ -6,8 +6,10 @@ import Navbar from '@/components/ui/navbar'
 import { isWeapp } from '@/utils/platform'
 import { Button } from '@/components/ui/button'
 import { authClient } from '@/api'
+import { useAuth } from '@/utils/auth'
 
 export default function WechatLogin() {
+  const { setUser } = useAuth()
   const [loading, setLoading] = useState(false)
   const [isWechatEnv, setIsWechatEnv] = useState(true)
 
@@ -76,17 +78,20 @@ export default function WechatLogin() {
 
       if (response.status === 200) {
         const { token, user, isNewUser } = await response.json()
-        
+
         // 4. 保存token和用户信息
         Taro.setStorageSync('userInfo', user)
         Taro.setStorageSync('mini_token', token) // 兼容RPC client的token存储
-        
+
+        // 更新 AuthContext 的用户状态
+        setUser(user as any) // 使用类型断言,mini-login 返回的用户类型与 me API 略有不同
+
         Taro.showToast({
           title: isNewUser ? '注册成功' : '登录成功',
           icon: 'success',
           duration: 1500
         })
-        
+
         // 跳转到首页
         setTimeout(() => {
           Taro.switchTab({ url: '/pages/index/index' })

+ 3 - 3
mini/src/pages/profile/index.tsx

@@ -429,7 +429,7 @@ const ProfilePage: React.FC = () => {
         </View>
 
         {/* 退出登录按钮 */}
-        <View className="px-4 pt-6 pb-8">
+        {/* <View className="px-4 pt-6 pb-8">
           <Button
             variant="destructive"
             size="lg"
@@ -441,12 +441,12 @@ const ProfilePage: React.FC = () => {
               退出登录
             </View>
           </Button>
-        </View>
+        </View> */}
 
         {/* 版本信息 */}
         <View className="pb-8">
           <Text className="text-center text-xs text-gray-400">
-            v0.0.17 - 小程序版
+            v0.0.18 - 小程序版
           </Text>
         </View>
         </View>

+ 10 - 7
mini/src/utils/auth.tsx

@@ -18,6 +18,7 @@ interface AuthContextType {
   register: (data: RegisterRequest) => Promise<User>
   updateUser: (userData: Partial<User>) => void
   refreshUser: () => Promise<User | null>
+  setUser: (user: User | null) => void // 添加 setUser 方法类型
   isLoading: boolean
   isLoggedIn: boolean
 }
@@ -101,7 +102,8 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
     }
   }
 
-  // 静默登录mutation - 应用启动时自动尝试登录
+  // 静默登录mutation - 移除自动登录,改为需要用户主动同意
+  // 保留此函数供需要时手动调用(如用户主动点击登录按钮)
   const silentLoginMutation = useMutation<User | null, Error, void>({
     mutationFn: async () => {
       // 使用统一的用户信息获取函数
@@ -114,12 +116,12 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
     }
   })
 
-  
-
-  // 在小程序启动时执行静默登录,刷新token和sessionKey
-  useLaunch(() => {
-    silentLoginMutation.mutate()
-  })
+  // 移除自动静默登录逻辑 - 小程序启动时不再自动登录
+  // 用户需要主动点击登录按钮并同意授权后才能登录
+  // 如需恢复自动登录,取消以下注释:
+  // useLaunch(() => {
+  //   silentLoginMutation.mutate()
+  // })
 
 
   const loginMutation = useMutation<User, Error, LoginRequest>({
@@ -246,6 +248,7 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
     register: registerMutation.mutateAsync,
     updateUser,
     refreshUser,
+    setUser, // 导出 setUser 方法供外部更新用户状态
     isLoading: loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending || silentLoginMutation.isPending,
     isLoggedIn: !!user,
   }

+ 3 - 12
web/src/client/admin/layouts/MainLayout.tsx

@@ -13,7 +13,6 @@ import { useAuth } from '../hooks/AuthProvider';
 import { useMenu, type MenuItem } from '../menu';
 import { getGlobalConfig } from '@/client/utils/utils';
 import { Button } from '@/client/components/ui/button';
-import { Input } from '@/client/components/ui/input';
 import { Avatar, AvatarFallback, AvatarImage } from '@/client/components/ui/avatar';
 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/client/components/ui/dropdown-menu';
 import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/client/components/ui/sheet';
@@ -168,20 +167,12 @@ export const MainLayout = () => {
           <div className="flex items-center gap-2">
             <Button
               variant="ghost"
-              size="icon"
-              className="md:hidden"
-              onClick={() => setIsMobileMenuOpen(true)}
-              data-testid="mobile-menu-button"
-            >
-              <Menu className="h-4 w-4" />
-            </Button>
-            <Button
-              variant="ghost"
-              size="icon"
-              className="hidden md:block"
+              size="sm"
               onClick={() => setCollapsed(!collapsed)}
+              data-testid="mobile-menu-button"
             >
               <Menu className="h-4 w-4" />
+              菜单栏
             </Button>
           </div>
 

+ 2 - 2
web/src/client/admin/menu.tsx

@@ -6,7 +6,6 @@ import {
   Settings,
   LogOut,
   File,
-  Megaphone,
   Package,
   Truck,
   TrendingUp,
@@ -21,6 +20,7 @@ export interface MenuItem {
   path?: string;
   permission?: string;
   onClick?: () => void;
+  type?: 'separator';
 }
 
 /**
@@ -263,7 +263,7 @@ export const useMenu = () => {
   ];
 
   // 用户菜单项
-  const userMenuItems = [
+  const userMenuItems: MenuItem[] = [
     // {
     //   key: 'profile',
     //   label: '个人资料',

BIN
多租户小程序-源代码-20251231-083528.tar.gz