|
|
@@ -0,0 +1,1193 @@
|
|
|
+---
|
|
|
+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'
|
|
|
+
|
|
|
+// 基础用法
|
|
|
+<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 组件
|
|
|
+```typescript
|
|
|
+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 组件
|
|
|
+```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: '' }
|
|
|
+})
|
|
|
+
|
|
|
+<Form {...form}>
|
|
|
+ <FormField
|
|
|
+ name="username"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>用户名</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入用户名" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+</Form>
|
|
|
+```
|
|
|
+
|
|
|
+### Card 组件
|
|
|
+```typescript
|
|
|
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
|
+
|
|
|
+<Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>卡片标题</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Text>卡片内容</Text>
|
|
|
+ </CardContent>
|
|
|
+</Card>
|
|
|
+```
|
|
|
+
|
|
|
+### Navbar 组件
|
|
|
+```typescript
|
|
|
+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 组件
|
|
|
+```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 (
|
|
|
+ <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. 创建页面目录
|
|
|
+```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 (
|
|
|
+ <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页面模板
|
|
|
+```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 (
|
|
|
+ <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. 页面配置文件
|
|
|
+```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 (
|
|
|
+ <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调用的页面模板
|
|
|
+```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<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. 带表单提交的页面模板
|
|
|
+```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<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 使用规范
|
|
|
+```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<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. 调用示例
|
|
|
+```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<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定义
|
|
|
+```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<typeof userSchema>
|
|
|
+```
|
|
|
+
|
|
|
+### 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<UserFormData>({
|
|
|
+ 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 (
|
|
|
+ <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页面标准结构
|
|
|
+```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 (
|
|
|
+ <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页面标准结构
|
|
|
+```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 (
|
|
|
+ <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页面配置
|
|
|
+```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)
|
|
|
+ }
|
|
|
+})
|