--- 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) } })