Browse Source

✨ feat(address): add address edit page

- add address edit page configuration file
- implement address edit page with form validation
- add address creation and update functionality
- implement region selection (province/city/district)
- add form data pre-filling for address editing

✨ refactor(goods-list): optimize goods list components

- replace native input with custom Input component
- replace native img with custom Image component
- optimize search input event handling
yourname 3 months ago
parent
commit
cb4155b0d8

+ 6 - 0
mini/src/pages/address-edit/index.config.ts

@@ -0,0 +1,6 @@
+export default definePageConfig({
+  navigationBarTitleText: '编辑地址',
+  enablePullDownRefresh: false,
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})

+ 284 - 0
mini/src/pages/address-edit/index.tsx

@@ -0,0 +1,284 @@
+import { View, ScrollView, Text } from '@tarojs/components'
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+import { useState, useEffect } from 'react'
+import Taro from '@tarojs/taro'
+import { deliveryAddressClient, cityClient } from '@/api'
+import { InferResponseType, InferRequestType } from 'hono'
+import { Navbar } from '@/components/ui/navbar'
+import { Card } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import { useAuth } from '@/utils/auth'
+
+type Address = InferResponseType<typeof deliveryAddressClient[':id']['$get'], 200>
+type CreateAddressRequest = InferRequestType<typeof deliveryAddressClient.$post>['json']
+type UpdateAddressRequest = InferRequestType<typeof deliveryAddressClient[':id']['$put']>['json']
+
+const addressSchema = z.object({
+  name: z.string().min(1, '请输入收货人姓名'),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号'),
+  province: z.number().positive('请选择省份'),
+  city: z.number().positive('请选择城市'),
+  district: z.number().positive('请选择区县'),
+  address: z.string().min(1, '请输入详细地址'),
+  isDefault: z.boolean().optional()
+})
+
+type AddressFormData = z.infer<typeof addressSchema>
+
+export default function AddressEditPage() {
+  const { user } = useAuth()
+  const queryClient = useQueryClient()
+  const [addressId, setAddressId] = useState<number | null>(null)
+  const [provinces, setProvinces] = useState<any[]>([])
+  const [cities, setCities] = useState<any[]>([])
+  const [districts, setDistricts] = useState<any[]>([])
+
+  // 获取地址ID
+  useEffect(() => {
+    const params = Taro.getCurrentInstance().router?.params
+    if (params?.id) {
+      setAddressId(parseInt(params.id))
+    }
+  }, [])
+
+  // 获取地址详情
+  const { data: address } = useQuery({
+    queryKey: ['address', addressId],
+    queryFn: async () => {
+      if (!addressId) return null
+      const response = await deliveryAddressClient[':id'].$get({
+        param: { id: addressId }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取地址失败')
+      }
+      return response.json()
+    },
+    enabled: !!addressId,
+  })
+
+  // 获取省份
+  const { data: provinceData } = useQuery({
+    queryKey: ['provinces'],
+    queryFn: async () => {
+      const response = await cityClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ parentId: 0 })
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取省份失败')
+      }
+      return response.json()
+    }
+  })
+
+  // 获取城市
+  const fetchCities = async (provinceId: number) => {
+    const response = await cityClient.$get({
+      query: {
+        page: 1,
+        pageSize: 100,
+        filters: JSON.stringify({ parentId: provinceId })
+      }
+    })
+    if (response.status !== 200) {
+      throw new Error('获取城市失败')
+    }
+    return response.json()
+  }
+
+  // 获取区县
+  const fetchDistricts = async (cityId: number) => {
+    const response = await cityClient.$get({
+      query: {
+        page: 1,
+        pageSize: 100,
+        filters: JSON.stringify({ parentId: cityId })
+      }
+    })
+    if (response.status !== 200) {
+      throw new Error('获取区县失败')
+    }
+    return response.json()
+  }
+
+  // 表单设置
+  const form = useForm<AddressFormData>({
+    resolver: zodResolver(addressSchema),
+    defaultValues: {
+      name: '',
+      phone: '',
+      province: 0,
+      city: 0,
+      district: 0,
+      address: '',
+      isDefault: false
+    }
+  })
+
+  // 填充表单数据
+  useEffect(() => {
+    if (address) {
+      form.reset({
+        name: address.name,
+        phone: address.phone,
+        province: address.receiverProvince,
+        city: address.receiverCity,
+        district: address.receiverDistrict,
+        address: address.address,
+        isDefault: address.isDefault === 1
+      })
+    }
+  }, [address, form])
+
+  // 创建/更新地址
+  const saveAddressMutation = useMutation({
+    mutationFn: async (data: AddressFormData) => {
+      const addressData: any = {
+        name: data.name,
+        phone: data.phone,
+        receiverProvince: data.province,
+        receiverCity: data.city,
+        receiverDistrict: data.district,
+        address: data.address,
+        userId: user?.id,
+        isDefault: data.isDefault ? 1 : 0
+      }
+
+      let response
+      if (addressId) {
+        response = await deliveryAddressClient[':id']['$put']({
+          param: { id: addressId },
+          json: addressData
+        })
+      } else {
+        response = await deliveryAddressClient.$post({ json: addressData })
+      }
+
+      if (response.status !== 200 && response.status !== 201) {
+        throw new Error('保存地址失败')
+      }
+      return response.json()
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['delivery-addresses'] })
+      Taro.showToast({
+        title: addressId ? '更新成功' : '添加成功',
+        icon: 'success'
+      })
+      Taro.navigateBack()
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '保存失败',
+        icon: 'none'
+      })
+    }
+  })
+
+  // 处理省份变化
+  const handleProvinceChange = async (provinceId: number) => {
+    if (provinceId) {
+      const response = await fetchCities(provinceId)
+      setCities(response.data || [])
+      setDistricts([])
+      form.setValue('city', 0)
+      form.setValue('district', 0)
+    }
+  }
+
+  // 处理城市变化
+  const handleCityChange = async (cityId: number) => {
+    if (cityId) {
+      const response = await fetchDistricts(cityId)
+      setDistricts(response.data || [])
+      form.setValue('district', 0)
+    }
+  }
+
+  // 初始化省份
+  useEffect(() => {
+    if (provinceData?.data) {
+      setProvinces(provinceData.data)
+    }
+  }, [provinceData])
+
+  // 加载城市数据
+  useEffect(() => {
+    const provinceId = form.watch('province')
+    if (provinceId) {
+      handleProvinceChange(provinceId)
+    }
+  }, [form.watch('province')])
+
+  // 加载区县数据
+  useEffect(() => {
+    const cityId = form.watch('city')
+    if (cityId) {
+      handleCityChange(cityId)
+    }
+  }, [form.watch('city')])
+
+  const onSubmit = (data: AddressFormData) => {
+    saveAddressMutation.mutate(data)
+  }
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <Navbar
+        title={addressId ? '编辑地址' : '添加地址'}
+        leftIcon="i-heroicons-chevron-left-20-solid"
+        onClickLeft={() => Taro.navigateBack()}
+      />
+      
+      <ScrollView className="h-screen pt-12 pb-20">
+        <View className="px-4 py-4">
+          <Form {...form}>
+            <View className="space-y-4">
+              <Card>
+                <View className="p-4 space-y-4">
+                  <FormField
+                    name="name"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>收货人姓名</FormLabel>
+                        <FormControl>
+                          <Input placeholder="请输入收货人姓名" {...field} />
+                        </FormControl>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+
+                  <FormField
+                    name="phone"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>手机号码</FormLabel>
+                        <FormControl>
+                          <Input placeholder="请输入手机号码" {...field} />
+                        </FormControl>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+
+                  <FormField
+                    name="province"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>省份</FormLabel>
+                        <FormControl>
+                          <picker
+                            mode="selector"
+                            range={provinces}
+                            range-key="name"
+                            value={provin

+ 5 - 3
mini/src/pages/goods-list/index.tsx

@@ -8,6 +8,8 @@ import { Navbar } from '@/components/ui/navbar'
 import { Card } from '@/components/ui/card'
 import { Card } from '@/components/ui/card'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { useCart } from '@/utils/cart'
 import { useCart } from '@/utils/cart'
+import { Image } from '@/components/ui/image'
+import { Input } from '@/components/ui/input'
 
 
 type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>
 type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>
 type Goods = GoodsResponse['data'][0]
 type Goods = GoodsResponse['data'][0]
@@ -107,12 +109,12 @@ export default function GoodsListPage() {
         <View className="px-4 py-4">
         <View className="px-4 py-4">
           {/* 搜索栏 */}
           {/* 搜索栏 */}
           <View className="bg-white rounded-lg p-3 mb-4 shadow-sm">
           <View className="bg-white rounded-lg p-3 mb-4 shadow-sm">
-            <input
+            <Input
               type="text"
               type="text"
               placeholder="搜索商品"
               placeholder="搜索商品"
               className="w-full outline-none"
               className="w-full outline-none"
               value={searchKeyword}
               value={searchKeyword}
-              onChange={(e) => setSearchKeyword(e.detail.value)}
+              onChange={(value) => setSearchKeyword(value)}
               onConfirm={() => refetch()}
               onConfirm={() => refetch()}
             />
             />
           </View>
           </View>
@@ -132,7 +134,7 @@ export default function GoodsListPage() {
                       onClick={() => handleGoodsClick(goods)}
                       onClick={() => handleGoodsClick(goods)}
                     >
                     >
                       {goods.imageFile?.fullUrl ? (
                       {goods.imageFile?.fullUrl ? (
-                        <img 
+                        <Image 
                           src={goods.imageFile.fullUrl} 
                           src={goods.imageFile.fullUrl} 
                           className="w-full h-full object-cover rounded-lg"
                           className="w-full h-full object-cover rounded-lg"
                           mode="aspectFill"
                           mode="aspectFill"