---
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 组件
```typescript
import { Button } from '@/components/ui/button'
// 基础用法
// 不同尺寸
// 不同样式
```
### Input 组件
```typescript
import { Input } from '@/components/ui/input'
// 基础用法
// 受控组件
// 不同类型
```
### Form 组件
```typescript
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: '' }
})
```
### Card 组件
```typescript
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
卡片标题
卡片内容
```
### Navbar 组件
```typescript
import { Navbar } from '@/components/ui/navbar'
// 基础用法
// 带返回按钮
Taro.navigateBack()}
/>
// 带右侧操作
```
### Carousel 组件
```typescript
// 实际页面使用示例
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 (
)
}
```
## 页面类型分类
### 1. TabBar页面(底部导航页)
特点:
- 使用 `TabBarLayout` 布局组件
- 路径配置在 `mini/src/app.config.ts` 中的 `tabBar.list`
- 包含底部导航栏,用户可直接切换
- 通常包含 `Navbar` 顶部导航组件
- 示例页面:首页、发现、个人中心
### 2. 非TabBar页面(独立页面)
特点:
- 不使用 `TabBarLayout`,直接渲染内容
- 使用 `Navbar` 组件作为顶部导航
- 需要手动处理返回导航
- 示例页面:登录、注册、详情页
## 开发流程
### 1. 创建页面目录
```bash
# TabBar页面
mkdir -p mini/src/pages/[页面名称]
# 非TabBar页面
mkdir -p mini/src/pages/[页面名称]
```
### 2. 创建页面文件
#### TabBar页面模板
```typescript
// 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 (
console.log('点击右上角')}
leftIcon=""
/>
欢迎使用
这是一个使用shadui组件的TabBar页面
)
}
export default [页面名称]Page
```
#### 非TabBar页面模板
```typescript
// 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 (
这是一个使用shadui组件的非TabBar页面
)
}
```
### 3. 页面配置文件
```typescript
// mini/src/pages/[页面名称]/index.config.ts
export default definePageConfig({
navigationBarTitleText: '页面标题',
enablePullDownRefresh: true,
backgroundTextStyle: 'dark',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black'
})
```
### 4. 样式文件
统一使用tailwindcss类,index.css为空即可
```css
/* mini/src/pages/[页面名称]/index.css */
```
## 高级功能模板
### 1. 带认证的页面模板
```typescript
// 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 (
)
}
if (!user) return null
return (
欢迎, {user.username}
)
}
```
### 2. 带API调用的页面模板
```typescript
// 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
export default function UserListPage() {
const { data, isLoading, error } = useQuery({
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 (
)
}
if (error) {
return (
{error.message}
)
}
return (
{data?.data.map(user => (
{user.username}
))}
)
}
```
### 3. 带表单提交的页面模板
```typescript
// 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
export default function CreateUserPage() {
const [loading, setLoading] = useState(false)
const form = useForm({
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 (
)
}
```
## 认证功能使用
### 1. useAuth Hook 使用规范
```typescript
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. 页面权限控制
```typescript
// 在需要认证的页面顶部
const { user, isLoading, isLoggedIn } = useAuth()
useEffect(() => {
if (!isLoading && !isLoggedIn) {
Taro.navigateTo({ url: '/pages/login/index' })
}
}, [isLoading, isLoggedIn])
// 或者使用路由守卫模式
```
## RPC客户端调用规范
### 1. 客户端导入
```typescript
// 从api.ts导入对应的客户端
import { authClient, userClient, fileClient } from '@/api'
```
### 2. 类型提取
```typescript
import { InferResponseType, InferRequestType } from 'hono'
// 响应类型提取
type UserResponse = InferResponseType
type UserDetailResponse = InferResponseType
// 请求类型提取
type CreateUserRequest = InferRequestType['json']
type UpdateUserRequest = InferRequestType['json']
```
### 3. 调用示例
```typescript
// 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. 查询配置
```typescript
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. 变更操作
```typescript
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. 删除操作
```typescript
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)
```typescript
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)
```typescript
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()
}
}
```
#### 移动端分页页面模板
```typescript
// 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
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 (
{isLoading ? (
) : (
<>
{allGoods.map((item) => (
{item.name}
))}
{isFetchingNextPage && (
加载更多...
)}
{!hasNextPage && allGoods.length > 0 && (
没有更多了
)}
>
)}
)
}
```
## 表单处理规范
### 1. 表单Schema定义
```typescript
// 在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
```
### 2. 表单使用
```typescript
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { userSchema, type UserFormData } from '@/schemas/user.schema'
const form = useForm({
resolver: zodResolver(userSchema),
defaultValues: {
username: '',
email: '',
phone: ''
}
})
// 表单提交
const onSubmit = async (data: UserFormData) => {
try {
await mutation.mutateAsync(data)
} catch (error) {
console.error('表单提交失败:', error)
}
}
```
## 错误处理规范
### 1. 统一的错误处理
```typescript
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. 页面级错误处理
```typescript
const { data, isLoading, error } = useQuery({
// ...查询配置
})
if (error) {
return (
{error.message}
)
}
```
## 页面模板示例
### 1. TabBar页面标准结构
```typescript
// 示例:首页
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 (
console.log('点击通知')}
leftIcon=""
/>
欢迎使用
这是一个简洁优雅的小程序首页
)
}
export default HomePage
```
### 2. 非TabBar页面标准结构
```typescript
// 示例:登录页
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 (
{/* Logo区域 */}
欢迎回来
{/* 表单区域 */}
{/* 表单内容 */}
)
}
```
## 路由配置
### 1. TabBar页面配置
```typescript
// 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-50` 或 `bg-white`
### 3. 状态管理
- 使用React hooks进行状态管理
- 复杂状态使用Context API
- 用户信息使用 `useAuth` hook
### 4. 错误处理
- 使用Taro.showToast显示错误信息
- 网络请求使用try-catch包裹
- 提供友好的用户反馈
### 5. 性能优化
- 使用懒加载组件
- 避免不必要的重新渲染
- 合理使用useMemo和useCallback
## 常用工具函数
### 1. 页面跳转
```typescript
// 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. 用户交互
```typescript
// 显示提示
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. 本地存储
```typescript
// 存储数据
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)
}
})