Bladeren bron

✨ feat(mini): 实现兑换中心和优惠券功能

- 新增兑换中心页面,支持兑换码输入和兑换功能
- 新增我的券包页面,展示用户已领取的优惠券
- 实现首页广告轮播和热门券包展示功能
- 添加兑换码兑换和优惠券领取的API调用逻辑

✨ feat(api): 扩展API客户端支持更多服务

- 添加advertisementClient支持广告相关接口
- 添加wechatCouponStockClient支持优惠券批次接口
- 添加redemptionCodeClient支持兑换码接口
- 添加wechatCouponClient和couponLogClient支持优惠券相关操作

🔧 chore(config): 更新应用配置

- 添加兑换页面路由配置
- 更新tabBar配置,将"发现"改为"兑换"
- 修改首页配置,添加下拉刷新距离

📝 docs(commands): 添加tabbar布局组件使用文档

- 创建mini-tabbar-layout.md说明文档
- 说明一级页面如何导入和使用TabBarLayout组件
yourname 3 maanden geleden
bovenliggende
commit
56269d2b0a

+ 5 - 0
.roo/commands/mini-tabbar-layout.md

@@ -0,0 +1,5 @@
+---
+description: "tabbar布局组件使用指令"
+---
+
+一级页面需要 使用 import { TabBarLayout } from '@/layouts/tab-bar-layout'

+ 17 - 2
mini/src/api.ts

@@ -1,8 +1,23 @@
-import type { AuthRoutes, UserRoutes, RoleRoutes, FileRoutes } from '@/server/api'
+import type {
+  AuthRoutes,
+  UserRoutes,
+  RoleRoutes,
+  FileRoutes,
+  AdvertisementRoutes,
+  WechatCouponStockRoutes,
+  RedemptionCodeRoutes,
+  WechatCouponRoutes,
+  CouponLogRoutes
+} from '@/server/api'
 import { rpcClient } from './utils/rpc-client'
 
 // 创建各个模块的RPC客户端
 export const authClient = rpcClient<AuthRoutes>().api.v1.auth
 export const userClient = rpcClient<UserRoutes>().api.v1.users
 export const roleClient = rpcClient<RoleRoutes>().api.v1.roles
-export const fileClient = rpcClient<FileRoutes>().api.v1.files
+export const fileClient = rpcClient<FileRoutes>().api.v1.files
+export const advertisementClient = rpcClient<AdvertisementRoutes>().api.v1.advertisements
+export const wechatCouponStockClient = rpcClient<WechatCouponStockRoutes>().api.v1['wechat-coupon-stocks']
+export const redemptionCodeClient = rpcClient<RedemptionCodeRoutes>().api.v1['redemption-codes']
+export const wechatCouponClient = rpcClient<WechatCouponRoutes>().api.v1['wechat-coupons']
+export const couponLogClient = rpcClient<CouponLogRoutes>().api.v1['coupon-logs']

+ 4 - 3
mini/src/app.config.ts

@@ -5,7 +5,8 @@ export default defineAppConfig({
     'pages/profile/index',
     'pages/login/index',
     'pages/login/wechat-login',
-    'pages/register/index'
+    'pages/register/index',
+    'pages/duihuan/index'
   ],
   window: {
     backgroundTextStyle: 'light',
@@ -25,8 +26,8 @@ export default defineAppConfig({
         text: '首页'
       },
       {
-        pagePath: 'pages/explore/index',
-        text: '发现'
+        pagePath: 'pages/duihuan/index',
+        text: '兑换'
       },
       {
         pagePath: 'pages/profile/index',

+ 6 - 6
mini/src/layouts/tab-bar-layout.tsx

@@ -16,10 +16,10 @@ const tabBarItems: TabBarItem[] = [
     selectedIconClass: 'i-heroicons-home-20-solid',
   },
   {
-    key: 'explore',
-    title: '发现',
-    iconClass: 'i-heroicons-magnifying-glass-20-solid',
-    selectedIconClass: 'i-heroicons-magnifying-glass-20-solid',
+    key: 'duihuan',
+    title: '兑换',
+    iconClass: 'i-heroicons-ticket-20-solid',
+    selectedIconClass: 'i-heroicons-ticket-20-solid',
   },
   {
     key: 'profile',
@@ -36,8 +36,8 @@ export const TabBarLayout: React.FC<TabBarLayoutProps> = ({ children, activeKey
       case 'home':
         Taro.switchTab({ url: '/pages/index/index' })
         break
-      case 'explore':
-        Taro.switchTab({ url: '/pages/explore/index' })
+      case 'duihuan':
+        Taro.switchTab({ url: '/pages/duihuan/index' })
         break
       case 'profile':
         Taro.switchTab({ url: '/pages/profile/index' })

+ 5 - 0
mini/src/pages/duihuan/index.config.ts

@@ -0,0 +1,5 @@
+export default definePageConfig({
+  navigationBarTitleText: '兑换中心',
+  enablePullDownRefresh: false,
+  backgroundColor: '#f5f5f5'
+})

+ 311 - 0
mini/src/pages/duihuan/index.tsx

@@ -0,0 +1,311 @@
+import { useState } from 'react'
+import { View, Text, ScrollView, Input } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+import { redemptionCodeClient, wechatCouponClient } from '@/api'
+import { checkAuth, redirectToLogin, getCurrentUserId } from '@/utils/coupon-api'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
+import { cn } from '@/utils/cn'
+
+// 兑换码验证schema
+const redeemSchema = z.object({
+  code: z.string()
+    .min(1, '请输入兑换码')
+    .max(20, '兑换码不能超过20位')
+    .regex(/^[a-zA-Z0-9]+$/, '兑换码只能包含字母和数字')
+})
+
+type RedeemFormData = z.infer<typeof redeemSchema>
+
+interface WechatCoupon {
+  id: number
+  couponCode: string
+  couponType: string
+  couponAmount: number
+  couponName: string
+  couponDescription: string
+  couponStatus: number
+  validBeginTime: string
+  validEndTime: string
+  receiveTime: string
+}
+
+export default function DuihuanPage() {
+  const queryClient = useQueryClient()
+  const [activeTab, setActiveTab] = useState<'redeem' | 'myCoupons'>('redeem')
+
+  // 兑换码表单
+  const form = useForm<RedeemFormData>({
+    resolver: zodResolver(redeemSchema),
+    defaultValues: {
+      code: ''
+    }
+  })
+
+  // 获取我的优惠券列表
+  const { data: coupons, isLoading: couponsLoading } = useQuery({
+    queryKey: ['my-coupons'],
+    queryFn: async () => {
+      if (!checkAuth()) {
+        throw new Error('请先登录')
+      }
+      
+      const userId = getCurrentUserId()
+      if (!userId) throw new Error('请先登录')
+      
+      const response = await wechatCouponClient.$get({
+        query: {
+          page: 1,
+          pageSize: 50,
+          filters: JSON.stringify({ userId })
+        }
+      })
+      if (response.status !== 200) throw new Error('获取优惠券失败')
+      const result = await response.json()
+      return result.data as WechatCoupon[]
+    },
+    enabled: activeTab === 'myCoupons' && checkAuth()
+  })
+
+  // 兑换码兑换
+  const redeemMutation = useMutation({
+    mutationFn: async (code: string) => {
+      if (!checkAuth()) {
+        throw new Error('请先登录')
+      }
+      
+      const userId = getCurrentUserId()
+      if (!userId) {
+        throw new Error('请先登录')
+      }
+      
+      const response = await redemptionCodeClient.redeem.$post({
+        json: { code, userId }
+      })
+      if (response.status !== 200) {
+        const error = await response.json()
+        throw new Error(error.message || '兑换失败')
+      }
+      return response.json()
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['my-coupons'] })
+      form.reset()
+      Taro.showToast({
+        title: '兑换成功',
+        icon: 'success'
+      })
+    },
+    onError: (error) => {
+      if (error instanceof Error && error.message === '请先登录') {
+        Taro.showModal({
+          title: '提示',
+          content: '请先登录后再兑换优惠券',
+          success: (res) => {
+            if (res.confirm) {
+              redirectToLogin()
+            }
+          }
+        })
+      } else {
+        Taro.showToast({
+          title: error instanceof Error ? error.message : '兑换失败',
+          icon: 'none'
+        })
+      }
+    }
+  })
+
+  const handleRedeem = (data: RedeemFormData) => {
+    if (!checkAuth()) {
+      Taro.showModal({
+        title: '提示',
+        content: '请先登录后再兑换优惠券',
+        success: (res) => {
+          if (res.confirm) {
+            redirectToLogin()
+          }
+        }
+      })
+      return
+    }
+    redeemMutation.mutate(data.code)
+  }
+
+  const handleCouponUse = (coupon: WechatCoupon) => {
+    // 跳转到使用页面或显示使用说明
+    Taro.showModal({
+      title: '使用说明',
+      content: coupon.couponDescription,
+      showCancel: false
+    })
+  }
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('zh-CN')
+  }
+
+  const getStatusText = (status: number) => {
+    switch (status) {
+      case 0: return '未使用'
+      case 1: return '已使用'
+      case 2: return '已过期'
+      default: return '未知'
+    }
+  }
+
+  const getStatusColor = (status: number) => {
+    switch (status) {
+      case 0: return 'text-green-600'
+      case 1: return 'text-blue-600'
+      case 2: return 'text-gray-400'
+      default: return 'text-gray-600'
+    }
+  }
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      {/* 标签切换 */}
+      <View className="bg-white border-b">
+        <View className="flex">
+          <View
+            className={cn(
+              'flex-1 py-3 text-center text-sm font-medium',
+              activeTab === 'redeem'
+                ? 'text-red-500 border-b-2 border-red-500'
+                : 'text-gray-600'
+            )}
+            onClick={() => setActiveTab('redeem')}
+          >
+            兑换中心
+          </View>
+          <View
+            className={cn(
+              'flex-1 py-3 text-center text-sm font-medium',
+              activeTab === 'myCoupons'
+                ? 'text-red-500 border-b-2 border-red-500'
+                : 'text-gray-600'
+            )}
+            onClick={() => setActiveTab('myCoupons')}
+          >
+            我的券包
+          </View>
+        </View>
+      </View>
+
+      <ScrollView className="flex-1">
+        {activeTab === 'redeem' ? (
+          /* 兑换码输入 */
+          <View className="p-4">
+            <Card>
+              <CardHeader className="pb-4">
+                <Text className="text-lg font-bold text-gray-900">兑换码兑换</Text>
+                <Text className="text-sm text-gray-500">输入兑换码领取优惠券</Text>
+              </CardHeader>
+              <CardContent>
+                <Form {...form}>
+                  <FormField
+                    control={form.control}
+                    name="code"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>兑换码</FormLabel>
+                        <FormControl>
+                          <Input
+                            placeholder="请输入兑换码"
+                            {...field}
+                            className="h-10"
+                          />
+                        </FormControl>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+                  <Button
+                    type="submit"
+                    className="w-full h-10 bg-red-500 text-white mt-4"
+                    loading={redeemMutation.isPending}
+                    onClick={form.handleSubmit(handleRedeem)}
+                  >
+                    立即兑换
+                  </Button>
+                </Form>
+                
+                <View className="mt-6 p-4 bg-gray-50 rounded-lg">
+                  <Text className="text-sm text-gray-600 mb-2">兑换说明:</Text>
+                  <Text className="text-xs text-gray-500 leading-relaxed">
+                    • 兑换码区分大小写,请准确输入{'\n'}
+                    • 每个兑换码仅限使用一次{'\n'}
+                    • 兑换成功后可在"我的券包"中查看{'\n'}
+                    • 如有问题请联系客服
+                  </Text>
+                </View>
+              </CardContent>
+            </Card>
+          </View>
+        ) : (
+          /* 我的券包 */
+          <View className="p-4">
+            {couponsLoading ? (
+              <View className="flex items-center justify-center py-8">
+                <Text className="text-gray-400">加载中...</Text>
+              </View>
+            ) : coupons && coupons.length > 0 ? (
+              <View className="space-y-4">
+                {coupons.map((coupon) => (
+                  <Card key={coupon.id}>
+                    <CardContent className="p-4">
+                      <View className="flex justify-between items-start mb-2">
+                        <View>
+                          <Text className="text-lg font-bold text-gray-900">
+                            {coupon.couponName}
+                          </Text>
+                          <Text className="text-sm text-gray-600">
+                            {coupon.couponDescription}
+                          </Text>
+                        </View>
+                        <View className="text-right">
+                          <Text className="text-xl font-bold text-red-500">
+                            ¥{coupon.couponAmount}
+                          </Text>
+                          <Text className={cn('text-sm', getStatusColor(coupon.couponStatus))}>
+                            {getStatusText(coupon.couponStatus)}
+                          </Text>
+                        </View>
+                      </View>
+                      
+                      <View className="flex justify-between items-center text-xs text-gray-500">
+                        <Text>有效期:{formatDate(coupon.validBeginTime)} - {formatDate(coupon.validEndTime)}</Text>
+                        <Text>领取时间:{formatDate(coupon.receiveTime)}</Text>
+                      </View>
+                      
+                      {coupon.couponStatus === 0 && (
+                        <Button
+                          className="w-full h-8 bg-red-500 text-white text-sm mt-3"
+                          onClick={() => handleCouponUse(coupon)}
+                        >
+                          立即使用
+                        </Button>
+                      )}
+                    </CardContent>
+                  </Card>
+                ))}
+              </View>
+            ) : (
+              <View className="flex flex-col items-center justify-center py-12">
+                <View className="i-heroicons-ticket-20-solid w-16 h-16 text-gray-300 mb-4" />
+                <Text className="text-gray-400 mb-2">暂无优惠券</Text>
+                <Text className="text-sm text-gray-500">快去兑换中心看看吧</Text>
+              </View>
+            )}
+          </View>
+        )}
+      </ScrollView>
+    </View>
+  )
+}

+ 3 - 3
mini/src/pages/index/index.config.ts

@@ -1,6 +1,6 @@
-export default {
+export default definePageConfig({
   navigationBarTitleText: '首页',
   enablePullDownRefresh: true,
-  backgroundTextStyle: 'dark',
   backgroundColor: '#f5f5f5',
-}
+  onReachBottomDistance: 50
+})

+ 233 - 28
mini/src/pages/index/index.tsx

@@ -1,32 +1,237 @@
-import React from 'react'
-import { View, Text } from '@tarojs/components'
-import { TabBarLayout } from '@/layouts/tab-bar-layout'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import { Navbar } from '@/components/ui/navbar'
-import './index.css'
-
-const HomePage: React.FC = () => {
+import { useState } from 'react'
+import { View, Text, ScrollView, Swiper, SwiperItem, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { advertisementClient, wechatCouponStockClient } from '@/api'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import { cn } from '@/utils/cn'
+import { receiveCoupon, getCurrentUserId, checkAuth, redirectToLogin } from '@/utils/coupon-api'
+
+interface Advertisement {
+  id: number
+  title: string
+  imageUrl: string
+  linkUrl: string
+  description: string
+  sortOrder: number
+}
+
+interface WechatCouponStock {
+  id: number
+  stockName: string
+  stockDescription: string
+  stockType: string
+  couponAmount: number
+  couponTotal: number
+  couponRemaining: number
+  beginTime: string
+  endTime: string
+  status: number
+  coverImage: string
+}
+
+export default function IndexPage() {
+  const queryClient = useQueryClient()
+
+  // 获取广告列表
+  const { data: advertisements, isLoading: adsLoading } = useQuery({
+    queryKey: ['advertisements'],
+    queryFn: async () => {
+      const response = await advertisementClient.$get({
+        query: {
+          page: 1,
+          pageSize: 10,
+          filters: JSON.stringify({ status: 1 }),
+        },
+      })
+      if (response.status !== 200) throw new Error('获取广告失败')
+      const result = await response.json()
+      return result.data as Advertisement[]
+    },
+  })
+
+  // 获取代金券批次列表
+  const { data: stocks, isLoading: stocksLoading } = useQuery({
+    queryKey: ['wechat-coupon-stocks'],
+    queryFn: async () => {
+      const response = await wechatCouponStockClient.$get({
+        query: {
+          page: 1,
+          pageSize: 20,
+          filters: JSON.stringify({ status: 1 }),
+        },
+      })
+      if (response.status !== 200) throw new Error('获取批次失败')
+      const result = await response.json()
+      return result.data as WechatCouponStock[]
+    },
+  })
+
+  // 领取代金券
+  const receiveMutation = useMutation({
+    mutationFn: async (stockId: number) => {
+      const userId = getCurrentUserId()
+      if (!userId) {
+        throw new Error('请先登录')
+      }
+      const result = await receiveCoupon({ stockId, userId })
+      if (!result.success) {
+        throw new Error(result.message)
+      }
+      return result
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['wechat-coupon-stocks'] })
+      Taro.showToast({
+        title: '领取成功',
+        icon: 'success',
+      })
+    },
+    onError: (error) => {
+      if (error instanceof Error && error.message === '请先登录') {
+        Taro.showModal({
+          title: '提示',
+          content: '请先登录后再领取优惠券',
+          success: (res) => {
+            if (res.confirm) {
+              redirectToLogin()
+            }
+          }
+        })
+      } else {
+        Taro.showToast({
+          title: error instanceof Error ? error.message : '领取失败',
+          icon: 'none',
+        })
+      }
+    }
+  })
+
+  const handleReceiveCoupon = (stockId: number) => {
+    if (!checkAuth()) {
+      Taro.showModal({
+        title: '提示',
+        content: '请先登录后再领取优惠券',
+        success: (res) => {
+          if (res.confirm) {
+            redirectToLogin()
+          }
+        }
+      })
+      return
+    }
+    receiveMutation.mutate(stockId)
+  }
+
+  const handleAdClick = (ad: Advertisement) => {
+    if (ad.linkUrl) {
+      Taro.navigateTo({
+        url: `/pages/webview/index?url=${encodeURIComponent(ad.linkUrl)}`,
+      })
+    }
+  }
+
   return (
-    <TabBarLayout activeKey="home">
-      <Navbar
-        title="首页"
-        rightIcon="i-heroicons-bell-20-solid"
-        onClickRight={() => console.log('点击通知')}
-        leftIcon=""
-      />
-      <View className="px-4 py-4">
-        <Text className="text-2xl font-bold text-gray-900">欢迎使用</Text>
-        <View className="mt-4">
-          <Text className="text-gray-600">这是一个简洁优雅的小程序首页</Text>
-          <View className="mt-6">
-            <Label className="mb-2">搜索</Label>
-            <Input placeholder="搜索内容..." />
+    <View className="min-h-screen bg-gray-50">
+      {/* 广告轮播 */}
+      <View className="bg-white">
+        {adsLoading ? (
+          <View className="h-48 flex items-center justify-center">
+            <Text className="text-gray-400">加载中...</Text>
           </View>
-        </View>
+        ) : advertisements && advertisements.length > 0 ? (
+          <Swiper
+            className="h-48"
+            indicatorColor="#999"
+            indicatorActiveColor="#333"
+            circular
+            autoplay
+          >
+            {advertisements.map((ad) => (
+              <SwiperItem key={ad.id} onClick={() => handleAdClick(ad)}>
+                <Image
+                  src={ad.imageUrl}
+                  className="w-full h-full"
+                  mode="aspectFill"
+                />
+              </SwiperItem>
+            ))}
+          </Swiper>
+        ) : (
+          <View className="h-48 flex items-center justify-center bg-gray-100">
+            <Text className="text-gray-400">暂无广告</Text>
+          </View>
+        )}
       </View>
-    </TabBarLayout>
-  )
-}
 
-export default HomePage
+      {/* 代金券批次列表 */}
+      <ScrollView className="flex-1 p-4">
+        <View className="mb-4">
+          <Text className="text-lg font-bold text-gray-900">热门券包</Text>
+          <Text className="text-sm text-gray-500">限时领取,先到先得</Text>
+        </View>
+
+        {stocksLoading ? (
+          <View className="flex items-center justify-center py-8">
+            <Text className="text-gray-400">加载中...</Text>
+          </View>
+        ) : stocks && stocks.length > 0 ? (
+          <View className="space-y-4">
+            {stocks.map((stock) => (
+              <Card key={stock.id} className="overflow-hidden">
+                <CardHeader className="p-0">
+                  <Image
+                    src={stock.coverImage}
+                    className="w-full h-32"
+                    mode="aspectFill"
+                  />
+                </CardHeader>
+                <CardContent className="p-4">
+                  <View className="flex justify-between items-start mb-2">
+                    <Text className="text-lg font-bold text-gray-900">
+                      {stock.stockName}
+                    </Text>
+                    <Text className="text-lg font-bold text-red-500">
+                      ¥{stock.couponAmount}
+                    </Text>
+                  </View>
+                  
+                  <Text className="text-sm text-gray-600 mb-2">
+                    {stock.stockDescription}
+                  </Text>
+                  
+                  <View className="flex justify-between items-center">
+                    <Text className="text-xs text-gray-500">
+                      剩余: {stock.couponRemaining}/{stock.couponTotal}
+                    </Text>
+                    <Text className="text-xs text-gray-500">
+                      {new Date(stock.endTime).toLocaleDateString()} 截止
+                    </Text>
+                  </View>
+                  
+                  <Button
+                    className={cn(
+                      'w-full mt-3 h-9',
+                      stock.couponRemaining > 0
+                        ? 'bg-red-500 text-white'
+                        : 'bg-gray-300 text-gray-500'
+                    )}
+                    disabled={stock.couponRemaining === 0}
+                    onClick={() => handleReceiveCoupon(stock.id)}
+                  >
+                    {stock.couponRemaining > 0 ? '立即领取' : '已领完'}
+                  </Button>
+                </CardContent>
+              </Card>
+            ))}
+          </View>
+        ) : (
+          <View className="flex items-center justify-center py-8">
+            <Text className="text-gray-400">暂无券包</Text>
+          </View>
+        )}
+      </ScrollView>
+    </View>
+  )
+}

+ 83 - 0
mini/src/utils/coupon-api.ts

@@ -0,0 +1,83 @@
+import Taro from '@tarojs/taro'
+import { wechatCouponClient } from '@/api'
+
+interface ReceiveCouponParams {
+  stockId: number
+  userId: number
+}
+
+interface ReceiveCouponResponse {
+  success: boolean
+  message: string
+  coupon?: {
+    id: number
+    couponCode: string
+    couponName: string
+    couponAmount: number
+    validBeginTime: string
+    validEndTime: string
+  }
+}
+
+/**
+ * 领取代金券
+ */
+export async function receiveCoupon(params: ReceiveCouponParams): Promise<ReceiveCouponResponse> {
+  try {
+    const response = await wechatCouponClient.$post({
+      json: {
+        stockId: params.stockId,
+        userId: params.userId
+      }
+    })
+
+    if (response.status === 200) {
+      const result = await response.json()
+      return {
+        success: true,
+        message: '领取成功',
+        coupon: result
+      }
+    } else if (response.status === 400) {
+      const error = await response.json()
+      return {
+        success: false,
+        message: error.message || '领取失败'
+      }
+    } else {
+      throw new Error('服务器错误')
+    }
+  } catch (error) {
+    console.error('领取代金券错误:', error)
+    return {
+      success: false,
+      message: error instanceof Error ? error.message : '网络错误'
+    }
+  }
+}
+
+/**
+ * 获取用户ID
+ */
+export function getCurrentUserId(): number | null {
+  const userInfo = Taro.getStorageSync('userInfo')
+  return userInfo?.id || null
+}
+
+/**
+ * 检查用户是否登录
+ */
+export function checkAuth(): boolean {
+  const token = Taro.getStorageSync('mini_token')
+  const userInfo = Taro.getStorageSync('userInfo')
+  return !!(token && userInfo?.id)
+}
+
+/**
+ * 跳转到登录页
+ */
+export function redirectToLogin() {
+  Taro.navigateTo({
+    url: '/pages/login/index'
+  })
+}