mini-shadui-页面开发.md 29 KB


description: "小程序shadui页面的开发指令"

小程序页面开发指令

概述

本指令规范了基于Taro + React + Shadui + Tailwind CSS的小程序页面开发流程,包含tabbar页和非tabbar页的创建标准和最佳实践,涵盖了认证、RPC调用、React Query v5使用等核心功能。

小程序Shadui路径

mini/src/components/ui

当前可用的Shadui组件

基于项目实际文件,当前小程序可用的shadui组件如下:

基础组件

  • Button - 按钮组件 (button.tsx)
  • Card - 卡片组件 (card.tsx)
  • Input - 输入框组件 (input.tsx)
  • Label - 标签组件 (label.tsx)
  • Form - 表单组件 (form.tsx)

交互组件

  • AvatarUpload - 头像上传组件 (avatar-upload.tsx)
  • Carousel - 轮播图组件 (carousel.tsx)
  • Image - 图片组件 (image.tsx)

导航组件

  • Navbar - 顶部导航栏组件 (navbar.tsx)
  • TabBar - 底部标签栏组件 (tab-bar.tsx)

布局组件

  • TabBarLayout: 用于tabbar页面,包含底部导航

  • 根据需求可扩展更多业务组件

组件使用示例

Button 组件

import { Button } from '@/components/ui/button'

// 基础用法
<Button onClick={handleClick}>主要按钮</Button>

// 不同尺寸
<Button size="sm">小按钮</Button>
<Button size="md">中按钮</Button>
<Button size="lg">大按钮</Button>

// 不同样式
<Button variant="primary">主要按钮</Button>
<Button variant="secondary">次要按钮</Button>
<Button variant="outline">边框按钮</Button>
<Button variant="ghost">幽灵按钮</Button>

Input 组件

import { Input } from '@/components/ui/input'

// 基础用法
<Input placeholder="请输入内容" />

// 受控组件
<Input value={value} onChange={handleChange} />

// 不同类型
<Input type="text" placeholder="文本输入" />
<Input type="number" placeholder="数字输入" />
<Input type="password" placeholder="密码输入" />

Form 组件

import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const formSchema = z.object({
  username: z.string().min(2, '用户名至少2个字符'),
  email: z.string().email('请输入有效的邮箱地址')
})

const form = useForm({
  resolver: zodResolver(formSchema),
  defaultValues: { username: '', email: '' }
})

<Form {...form}>
  <FormField
    name="username"
    render={({ field }) => (
      <FormItem>
        <FormLabel>用户名</FormLabel>
        <FormControl>
          <Input placeholder="请输入用户名" {...field} />
        </FormControl>
        <FormMessage />
      </FormItem>
    )}
  />
</Form>

Card 组件

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

<Card>
  <CardHeader>
    <CardTitle>卡片标题</CardTitle>
  </CardHeader>
  <CardContent>
    <Text>卡片内容</Text>
  </CardContent>
</Card>

Navbar 组件

import { Navbar } from '@/components/ui/navbar'

// 基础用法
<Navbar title="页面标题" />

// 带返回按钮
<Navbar 
  title="页面标题" 
  leftIcon="i-heroicons-chevron-left-20-solid"
  onClickLeft={() => Taro.navigateBack()}
/>

// 带右侧操作
<Navbar 
  title="页面标题"
  rightIcon="i-heroicons-share-20-solid"
  onClickRight={handleShare}
/>

Carousel 组件

// 实际页面使用示例
export function HomeCarousel() {
  const bannerItems: CarouselItem[] = [
    {
      src: 'https://via.placeholder.com/750x400/3B82F6/FFFFFF?text=Banner+1',
      title: '新品上市',
      description: '最新款式,限时优惠',
      link: '/pages/goods/new-arrival'
    },
    {
      src: 'https://via.placeholder.com/750x400/EF4444/FFFFFF?text=Banner+2',
      title: '限时秒杀',
      description: '每日特价,不容错过',
      link: '/pages/goods/flash-sale'
    },
    {
      src: 'https://via.placeholder.com/750x400/10B981/FFFFFF?text=Banner+3',
      title: '会员专享',
      description: '会员专享折扣和福利',
      link: '/pages/member/benefits'
    }
  ]

  const handleBannerClick = (item: CarouselItem, index: number) => {
    if (item.link) {
      // 使用Taro跳转
      Taro.navigateTo({
        url: item.link
      })
    }
  }

  return (
    <View className="w-full">
      <Carousel
        items={bannerItems}
        height={400}
        autoplay={true}
        interval={4000}
        circular={true}
        rounded="none"
        onItemClick={handleBannerClick}
      />
    </View>
  )
}

页面类型分类

1. TabBar页面(底部导航页)

特点:

  • 使用 TabBarLayout 布局组件
  • 路径配置在 mini/src/app.config.ts 中的 tabBar.list
  • 包含底部导航栏,用户可直接切换
  • 通常包含 Navbar 顶部导航组件
  • 示例页面:首页、发现、个人中心

2. 非TabBar页面(独立页面)

特点:

  • 不使用 TabBarLayout,直接渲染内容
  • 使用 Navbar 组件作为顶部导航
  • 需要手动处理返回导航
  • 示例页面:登录、注册、详情页

开发流程

1. 创建页面目录

# TabBar页面
mkdir -p mini/src/pages/[页面名称]

# 非TabBar页面
mkdir -p mini/src/pages/[页面名称]

2. 创建页面文件

TabBar页面模板

// mini/src/pages/[页面名称]/index.tsx
import React from 'react'
import { View, Text } from '@tarojs/components'
import { TabBarLayout } from '@/layouts/tab-bar-layout'
import { Navbar } from '@/components/ui/navbar'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import './index.css'

const [页面名称]Page: React.FC = () => {
  return (
    <TabBarLayout activeKey="[对应tabBar.key]">
      <Navbar
        title="页面标题"
        rightIcon="i-heroicons-[图标名称]-20-solid"
        onClickRight={() => console.log('点击右上角')}
        leftIcon=""
      />
      <View className="px-4 py-4">
        <Card>
          <CardHeader>
            <CardTitle>欢迎使用</CardTitle>
          </CardHeader>
          <CardContent>
            <Text>这是一个使用shadui组件的TabBar页面</Text>
            <Button className="mt-4">开始使用</Button>
          </CardContent>
        </Card>
      </View>
    </TabBarLayout>
  )
}

export default [页面名称]Page

非TabBar页面模板

// mini/src/pages/[页面名称]/index.tsx
import { View } from '@tarojs/components'
import { useEffect } from 'react'
import Taro from '@tarojs/taro'
import { Navbar } from '@/components/ui/navbar'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import './index.css'

export default function [页面名称]() {
  useEffect(() => {
    Taro.setNavigationBarTitle({
      title: '页面标题'
    })
  }, [])

  return (
    <View className="min-h-screen bg-gray-50">
      <Navbar
        title="页面标题"
        backgroundColor="bg-transparent"
        textColor="text-gray-900"
        border={false}
      />
      <View className="px-6 py-4">
        <Card>
          <CardContent>
            <Text>这是一个使用shadui组件的非TabBar页面</Text>
            <Button className="mt-4">返回</Button>
          </CardContent>
        </Card>
      </View>
    </View>
  )
}

3. 页面配置文件

// mini/src/pages/[页面名称]/index.config.ts
export default definePageConfig({
  navigationBarTitleText: '页面标题',
  enablePullDownRefresh: true,
  backgroundTextStyle: 'dark',
  navigationBarBackgroundColor: '#ffffff',
  navigationBarTextStyle: 'black'
})

4. 样式文件

统一使用tailwindcss类,index.css为空即可

/* mini/src/pages/[页面名称]/index.css */

高级功能模板

1. 带认证的页面模板

// mini/src/pages/[需要认证的页面]/index.tsx
import { View, Text } from '@tarojs/components'
import { useEffect } from 'react'
import { useAuth } from '@/utils/auth'
import Taro from '@tarojs/taro'
import { Navbar } from '@/components/ui/navbar'
import { Card } from '@/components/ui/card'

export default function ProtectedPage() {
  const { user, isLoading, isLoggedIn } = useAuth()

  useEffect(() => {
    if (!isLoading && !isLoggedIn) {
      Taro.navigateTo({ url: '/pages/login/index' })
    }
  }, [isLoading, isLoggedIn])

  if (isLoading) {
    return (
      <View className="flex-1 flex items-center justify-center">
        <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
      </View>
    )
  }

  if (!user) return null

  return (
    <View className="min-h-screen bg-gray-50">
      <Navbar title="受保护页面" leftIcon="" />
      <View className="px-4 py-4">
        <Text>欢迎, {user.username}</Text>
      </View>
    </View>
  )
}

2. 带API调用的页面模板

// mini/src/pages/[数据展示页面]/index.tsx
import { View, ScrollView } from '@tarojs/components'
import { useQuery } from '@tanstack/react-query'
import { userClient } from '@/api'
import { InferResponseType } from 'hono'
import Taro from '@tarojs/taro'

type UserListResponse = InferResponseType<typeof userClient.$get, 200>

export default function UserListPage() {
  const { data, isLoading, error } = useQuery<UserListResponse>({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await userClient.$get({})
      if (response.status !== 200) {
        throw new Error('获取用户列表失败')
      }
      return response.json()
    },
    staleTime: 5 * 60 * 1000, // 5分钟
  })

  if (isLoading) {
    return (
      <View className="flex-1 flex items-center justify-center">
        <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
      </View>
    )
  }

  if (error) {
    return (
      <View className="flex-1 flex items-center justify-center">
        <Text className="text-red-500">{error.message}</Text>
      </View>
    )
  }

  return (
    <ScrollView className="h-screen">
      <Navbar title="用户列表" leftIcon="" />
      <View className="px-4 py-4">
        {data?.data.map(user => (
          <View key={user.id} className="bg-white rounded-lg p-4 mb-3">
            <Text>{user.username}</Text>
          </View>
        ))}
      </View>
    </ScrollView>
  )
}

3. 带表单提交的页面模板

// mini/src/pages/[表单页面]/index.tsx
import { View } from '@tarojs/components'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { useMutation } from '@tanstack/react-query'
import { userClient } from '@/api'
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import Taro from '@tarojs/taro'

const formSchema = z.object({
  username: z.string().min(3, '用户名至少3个字符'),
  email: z.string().email('请输入有效的邮箱地址'),
  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号')
})

type FormData = z.infer<typeof formSchema>

export default function CreateUserPage() {
  const [loading, setLoading] = useState(false)
  
  const form = useForm<FormData>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: '',
      email: '',
      phone: ''
    }
  })

  const mutation = useMutation({
    mutationFn: async (data: FormData) => {
      const response = await userClient.$post({ json: data })
      if (response.status !== 201) {
        throw new Error('创建用户失败')
      }
      return response.json()
    },
    onSuccess: () => {
      Taro.showToast({
        title: '创建成功',
        icon: 'success'
      })
      Taro.navigateBack()
    },
    onError: (error) => {
      Taro.showToast({
        title: error.message || '创建失败',
        icon: 'none'
      })
    }
  })

  const onSubmit = async (data: FormData) => {
    setLoading(true)
    try {
      await mutation.mutateAsync(data)
    } finally {
      setLoading(false)
    }
  }

  return (
    <View className="min-h-screen bg-gray-50">
      <Navbar title="创建用户" leftIcon="" />
      <View className="px-4 py-4">
        <Form {...form}>
          <View className="space-y-4">
            <FormField
              control={form.control}
              name="username"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>用户名</FormLabel>
                  <FormControl>
                    <Input placeholder="请输入用户名" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <Button
              className="w-full"
              onClick={form.handleSubmit(onSubmit)}
              disabled={loading}
            >
              {loading ? '创建中...' : '创建用户'}
            </Button>
          </View>
        </Form>
      </View>
    </View>
  )
}

认证功能使用

1. useAuth Hook 使用规范

import { useAuth } from '@/utils/auth'

// 在页面或组件中使用
const { 
  user,           // 当前用户信息
  login,          // 登录函数
  logout,         // 登出函数
  register,       // 注册函数
  updateUser,     // 更新用户信息
  isLoading,      // 加载状态
  isLoggedIn      // 是否已登录
} = useAuth()

// 使用示例
const handleLogin = async (formData) => {
  try {
    await login(formData)
    Taro.switchTab({ url: '/pages/index/index' })
  } catch (error) {
    console.error('登录失败:', error)
  }
}

2. 页面权限控制

// 在需要认证的页面顶部
const { user, isLoading, isLoggedIn } = useAuth()

useEffect(() => {
  if (!isLoading && !isLoggedIn) {
    Taro.navigateTo({ url: '/pages/login/index' })
  }
}, [isLoading, isLoggedIn])

// 或者使用路由守卫模式

RPC客户端调用规范

1. 客户端导入

// 从api.ts导入对应的客户端
import { authClient, userClient, fileClient } from '@/api'

2. 类型提取

import { InferResponseType, InferRequestType } from 'hono'

// 响应类型提取
type UserResponse = InferResponseType<typeof userClient.$get, 200>
type UserDetailResponse = InferResponseType<typeof userClient[':id']['$get'], 200>

// 请求类型提取
type CreateUserRequest = InferRequestType<typeof userClient.$post>['json']
type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['json']

3. 调用示例

// GET请求 - 列表
const response = await userClient.$get({
  query: {
    page: 1,
    pageSize: 10,
    keyword: 'search'
  }
})

// GET请求 - 单条
const response = await userClient[':id'].$get({
  param: { id: userId }
})

// POST请求
const response = await userClient.$post({
  json: {
    username: 'newuser',
    email: 'user@example.com'
  }
})

// PUT请求
const response = await userClient[':id'].$put({
  param: { id: userId },
  json: { username: 'updated' }
})

// DELETE请求
const response = await userClient[':id'].$delete({
  param: { id: userId }
})

React Query v5使用规范

1. 查询配置

const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['users', page, keyword], // 唯一的查询键
  queryFn: async () => {
    const response = await userClient.$get({
      query: { page, pageSize: 10, keyword }
    })
    if (response.status !== 200) {
      throw new Error('获取数据失败')
    }
    return response.json()
  },
  staleTime: 5 * 60 * 1000, // 5分钟
  cacheTime: 10 * 60 * 1000, // 10分钟
  retry: 3, // 重试3次
  enabled: !!keyword, // 条件查询
})

2. 变更操作

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: async (data: CreateUserRequest) => {
    const response = await userClient.$post({ json: data })
    if (response.status !== 201) {
      throw new Error('创建失败')
    }
    return response.json()
  },
  onSuccess: () => {
    // 成功后刷新相关查询
    queryClient.invalidateQueries({ queryKey: ['users'] })
    Taro.showToast({ title: '创建成功', icon: 'success' })
  },
  onError: (error) => {
    Taro.showToast({ 
      title: error.message || '操作失败', 
      icon: 'none' 
    })
  }
})

3. 删除操作

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: async (id: number) => {
    const response = await deliveryAddressClient[':id'].$delete({
      param: { id }
    })
    if (response.status !== 204) {
      throw new Error('删除地址失败')
    }
    return response.json()
  },
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['delivery-addresses'] })
    Taro.showToast({
      title: '删除成功',
      icon: 'success'
    })
  },
  onError: (error) => {
    Taro.showToast({
      title: error.message || '删除失败',
      icon: 'none'
    })
  }
})

4. 分页查询

标准分页(useQuery)

const useUserList = (page: number, pageSize: number = 10) => {
  return useQuery({
    queryKey: ['users', page, pageSize],
    queryFn: async () => {
      const response = await userClient.$get({
        query: { page, pageSize }
      })
      return response.json()
    },
    keepPreviousData: true, // 保持上一页数据
  })
}

移动端无限滚动分页(useInfiniteQuery)

import { useInfiniteQuery } from '@tanstack/react-query'

const useInfiniteUserList = (keyword?: string) => {
  return useInfiniteQuery({
    queryKey: ['users-infinite', keyword],
    queryFn: async ({ pageParam = 1 }) => {
      const response = await userClient.$get({
        query: {
          page: pageParam,
          pageSize: 10,
          keyword
        }
      })
      if (response.status !== 200) {
        throw new Error('获取用户列表失败')
      }
      return response.json()
    },
    getNextPageParam: (lastPage, allPages) => {
      const totalPages = Math.ceil(lastPage.pagination.total / lastPage.pagination.pageSize)
      const nextPage = allPages.length + 1
      return nextPage <= totalPages ? nextPage : undefined
    },
    staleTime: 5 * 60 * 1000,
  })
}

// 使用示例
const {
  data,
  isLoading,
  isFetchingNextPage,
  fetchNextPage,
  hasNextPage,
  refetch
} = useInfiniteUserList(searchKeyword)

// 合并所有分页数据
const allUsers = data?.pages.flatMap(page => page.data) || []

// 触底加载更多处理
const handleScrollToLower = () => {
  if (hasNextPage && !isFetchingNextPage) {
    fetchNextPage()
  }
}

移动端分页页面模板

// mini/src/pages/[无限滚动列表]/index.tsx
import { View, ScrollView } from '@tarojs/components'
import { useInfiniteQuery } from '@tanstack/react-query'
import { goodsClient } from '@/api'
import { InferResponseType } from 'hono'
import Taro from '@tarojs/taro'

type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>

export default function InfiniteGoodsList() {
  const [searchKeyword, setSearchKeyword] = useState('')

  const {
    data,
    isLoading,
    isFetchingNextPage,
    fetchNextPage,
    hasNextPage,
    refetch
  } = useInfiniteQuery({
    queryKey: ['goods-infinite', searchKeyword],
    queryFn: async ({ pageParam = 1 }) => {
      const response = await goodsClient.$get({
        query: {
          page: pageParam,
          pageSize: 10,
          keyword: searchKeyword
        }
      })
      if (response.status !== 200) {
        throw new Error('获取商品失败')
      }
      return response.json()
    },
    getNextPageParam: (lastPage) => {
      const { pagination } = lastPage
      const totalPages = Math.ceil(pagination.total / pagination.pageSize)
      return pagination.current < totalPages ? pagination.current + 1 : undefined
    },
    staleTime: 5 * 60 * 1000,
    initialPageParam: 1,
  })

  // 合并所有分页数据
  const allGoods = data?.pages.flatMap(page => page.data) || []

  // 触底加载更多
  const handleScrollToLower = () => {
    if (hasNextPage && !isFetchingNextPage) {
      fetchNextPage()
    }
  }

  // 下拉刷新
  const onPullDownRefresh = () => {
    refetch().finally(() => {
      Taro.stopPullDownRefresh()
    })
  }

  return (
    <ScrollView
      className="h-screen"
      scrollY
      onScrollToLower={handleScrollToLower}
      refresherEnabled
      refresherTriggered={false}
      onRefresherRefresh={onPullDownRefresh}
    >
      <View className="px-4 py-4">
        {isLoading ? (
          <View className="flex justify-center py-10">
            <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
          </View>
        ) : (
          <>
            {allGoods.map((item) => (
              <View key={item.id} className="bg-white rounded-lg p-4 mb-3">
                <Text>{item.name}</Text>
              </View>
            ))}
            
            {isFetchingNextPage && (
              <View className="flex justify-center py-4">
                <View className="i-heroicons-arrow-path-20-solid animate-spin w-6 h-6 text-blue-500" />
                <Text className="ml-2 text-sm text-gray-500">加载更多...</Text>
              </View>
            )}
            
            {!hasNextPage && allGoods.length > 0 && (
              <View className="text-center py-4 text-sm text-gray-400">
                没有更多了
              </View>
            )}
          </>
        )}
      </View>
    </ScrollView>
  )
}

表单处理规范

1. 表单Schema定义

// 在schemas目录下定义
import { z } from 'zod'

export const userSchema = z.object({
  username: z.string()
    .min(3, '用户名至少3个字符')
    .max(20, '用户名最多20个字符')
    .regex(/^\S+$/, '用户名不能包含空格'),
  email: z.string().email('请输入有效的邮箱地址'),
  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号')
})

export type UserFormData = z.infer<typeof userSchema>

2. 表单使用

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { userSchema, type UserFormData } from '@/schemas/user.schema'

const form = useForm<UserFormData>({
  resolver: zodResolver(userSchema),
  defaultValues: {
    username: '',
    email: '',
    phone: ''
  }
})

// 表单提交
const onSubmit = async (data: UserFormData) => {
  try {
    await mutation.mutateAsync(data)
  } catch (error) {
    console.error('表单提交失败:', error)
  }
}

错误处理规范

1. 统一的错误处理

const handleApiError = (error: any) => {
  const message = error.response?.data?.message || error.message || '操作失败'
  
  if (error.response?.status === 401) {
    Taro.showModal({
      title: '未登录',
      content: '请先登录',
      success: () => {
        Taro.navigateTo({ url: '/pages/login/index' })
      }
    })
  } else if (error.response?.status === 403) {
    Taro.showToast({ title: '权限不足', icon: 'none' })
  } else if (error.response?.status === 404) {
    Taro.showToast({ title: '资源不存在', icon: 'none' })
  } else if (error.response?.status >= 500) {
    Taro.showToast({ title: '服务器错误,请稍后重试', icon: 'none' })
  } else {
    Taro.showToast({ title: message, icon: 'none' })
  }
}

2. 页面级错误处理

const { data, isLoading, error } = useQuery({
  // ...查询配置
})

if (error) {
  return (
    <View className="flex-1 flex items-center justify-center">
      <View className="text-center">
        <View className="i-heroicons-exclamation-triangle-20-solid w-12 h-12 text-red-500 mx-auto mb-4" />
        <Text className="text-gray-600 mb-4">{error.message}</Text>
        <Button onClick={() => refetch()}>重新加载</Button>
      </View>
    </View>
  )
}

页面模板示例

1. TabBar页面标准结构

// 示例:首页
import React from 'react'
import { View, Text } from '@tarojs/components'
import { TabBarLayout } from '@/layouts/tab-bar-layout'
import { Navbar } from '@/components/ui/navbar'
import { Button } from '@/components/ui/button'
import './index.css'

const HomePage: React.FC = () => {
  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>
      </View>
    </TabBarLayout>
  )
}

export default HomePage

2. 非TabBar页面标准结构

// 示例:登录页
import { View } from '@tarojs/components'
import { useEffect } from 'react'
import Taro from '@tarojs/taro'
import { Navbar } from '@/components/ui/navbar'
import { Button } from '@/components/ui/button'
import './index.css'

export default function Login() {
  useEffect(() => {
    Taro.setNavigationBarTitle({
      title: '用户登录'
    })
  }, [])

  return (
    <View className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
      <Navbar
        title="用户登录"
        backgroundColor="bg-transparent"
        textColor="text-gray-900"
        border={false}
      />
      <View className="flex-1 px-6 py-12">
        {/* Logo区域 */}
        <View className="flex flex-col items-center mb-10">
          <View className="w-20 h-20 mb-4 rounded-full bg-white shadow-lg flex items-center justify-center">
            <View className="i-heroicons-user-circle-20-solid w-12 h-12 text-blue-500" />
          </View>
          <Text className="text-2xl font-bold text-gray-900 mb-1">欢迎回来</Text>
        </View>

        {/* 表单区域 */}
        <View className="bg-white rounded-2xl shadow-sm p-6">
          <View className="space-y-5">
            {/* 表单内容 */}
          </View>
        </View>
      </View>
    </View>
  )
}

路由配置

1. TabBar页面配置

// mini/src/app.config.ts
export default defineAppConfig({
  pages: [
    'pages/index/index',
    'pages/explore/index',
    'pages/profile/index',
    // 其他页面
  ],
  tabBar: {
    color: '#666666',
    selectedColor: '#1976D2',
    backgroundColor: '#ffffff',
    borderStyle: 'black',
    list: [
      {
        pagePath: 'pages/index/index',
        text: '首页',
        iconPath: 'assets/icons/home.png',
        selectedIconPath: 'assets/icons/home-active.png'
      },
      {
        pagePath: 'pages/explore/index',
        text: '发现',
        iconPath: 'assets/icons/explore.png',
        selectedIconPath: 'assets/icons/explore-active.png'
      },
      {
        pagePath: 'pages/profile/index',
        text: '我的',
        iconPath: 'assets/icons/profile.png',
        selectedIconPath: 'assets/icons/profile-active.png'
      }
    ]
  }
})

2. 非TabBar页面路由

非TabBar页面会自动添加到pages数组中,无需额外配置tabBar。

最佳实践

1. 命名规范

  • 页面目录:使用小写+中划线命名,如 user-profile
  • 组件名称:使用PascalCase,如 UserProfilePage
  • 文件名:使用小写+中划线命名,如 user-profile.tsx

2. 样式规范

  • 使用Tailwind CSS原子类
  • 避免使用px,使用rpx单位
  • 页面背景色统一使用 bg-gray-50bg-white

3. 状态管理

  • 使用React hooks进行状态管理
  • 复杂状态使用Context API
  • 用户信息使用 useAuth hook

4. 错误处理

  • 使用Taro.showToast显示错误信息
  • 网络请求使用try-catch包裹
  • 提供友好的用户反馈

5. 性能优化

  • 使用懒加载组件
  • 避免不必要的重新渲染
  • 合理使用useMemo和useCallback

常用工具函数

1. 页面跳转

// Tab页面跳转
Taro.switchTab({ url: '/pages/index/index' })

// 普通页面跳转
Taro.navigateTo({ url: '/pages/login/index' })

// 返回上一页
Taro.navigateBack()

// 重定向(清除当前页面历史)
Taro.redirectTo({ url: '/pages/login/index' })

// 重新启动应用
Taro.reLaunch({ url: '/pages/index/index' })

2. 用户交互

// 显示提示
Taro.showToast({
  title: '操作成功',
  icon: 'success',
  duration: 2000
})

// 显示加载
Taro.showLoading({
  title: '加载中...',
  mask: true
})
Taro.hideLoading()

// 显示确认对话框
Taro.showModal({
  title: '确认操作',
  content: '确定要执行此操作吗?',
  success: (res) => {
    if (res.confirm) {
      // 用户点击确认
    }
  }
})

// 显示操作菜单
Taro.showActionSheet({
  itemList: ['选项1', '选项2', '选项3'],
  success: (res) => {
    console.log('用户选择了', res.tapIndex)
  }
})

3. 本地存储

// 存储数据
Taro.setStorageSync('key', 'value')
Taro.setStorageSync('user', JSON.stringify(user))

// 获取数据
const value = Taro.getStorageSync('key')
const user = JSON.parse(Taro.getStorageSync('user') || '{}')

// 移除数据
Taro.removeStorageSync('key')

// 清空所有数据
Taro.clearStorageSync()

4. 设备信息

```typescript // 获取系统信息 const systemInfo = Taro.getSystemInfoSync() const { screenWidth, screenHeight, windowWidth, windowHeight, statusBarHeight } = systemInfo

// 获取用户位置 Taro.getLocation({ type: 'wgs84', success: (res) => {

console.log('纬度:', res.latitude)
console.log('经度:', res.longitude)

} })