Procházet zdrojové kódy

📝 docs(mini-page): update mini program development documentation

- add advanced feature templates including authentication, API calls, and form submission
- add authentication feature usage guidelines with useAuth hook examples
- add RPC client calling specifications and type extraction methods
- add React Query v5 usage specifications for queries and mutations
- add form handling standards with Zod schema validation
- add error handling specifications and page-level error UI examples
- add navigation methods and user interaction examples
- update overview to mention authentication, RPC calls, and React Query v5

📝 docs(mini-check-curd-entity-schema): update entity schema check command

- add instruction to add missing rpc client definitions in mini/src/api.ts
yourname před 4 měsíci
rodič
revize
5719093932

+ 2 - 1
.roo/commands/mini-check-curd-entity-schema.md

@@ -2,4 +2,5 @@
 description: "检查页面相关的实体,schema, CRUD路由指令"
 ---
 
-检查页面相关的实体,schema, CRUD路由,src/server/api.ts, mini/src/api.ts中的rpc client 定义,以收集进行页面开发所需的上下文
+检查页面相关的实体,schema, CRUD路由,src/server/api.ts, mini/src/api.ts中的rpc client 定义,以收集进行页面开发所需的上下文
+如果 mini/src/api.ts中的rpc client 定义缺少,就先添加

+ 521 - 2
.roo/commands/mini-page.md

@@ -5,7 +5,7 @@ description: "小程序页面的开发指令"
 # 小程序页面开发指令
 
 ## 概述
-本指令规范了基于Taro + React + Tailwind CSS的小程序页面开发流程,包含tabbar页和非tabbar页的创建标准和最佳实践。
+本指令规范了基于Taro + React + Tailwind CSS的小程序页面开发流程,包含tabbar页和非tabbar页的创建标准和最佳实践,涵盖了认证、RPC调用、React Query v5使用等核心功能
 
 ## 页面类型分类
 
@@ -119,6 +119,456 @@ page {
 /* 自定义样式 */
 ```
 
+## 高级功能模板
+
+### 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'
+
+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 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, // 保持上一页数据
+  })
+}
+```
+
+## 表单处理规范
+
+### 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页面标准结构
@@ -297,9 +747,78 @@ Taro.navigateTo({ url: '/pages/login/index' })
 
 // 返回上一页
 Taro.navigateBack()
+
+// 重定向(清除当前页面历史)
+Taro.redirectTo({ url: '/pages/login/index' })
+
+// 重新启动应用
+Taro.reLaunch({ url: '/pages/index/index' })
 ```
 
 ### 2. 用户交互
 ```typescript
 // 显示提示
-Taro.showToast({
+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)
+  }
+})