index.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import { View, ScrollView, Text } from '@tarojs/components'
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
  3. import { useForm } from 'react-hook-form'
  4. import { zodResolver } from '@hookform/resolvers/zod'
  5. import { z } from 'zod'
  6. import { useState, useEffect } from 'react'
  7. import Taro from '@tarojs/taro'
  8. import { deliveryAddressClient, cityClient } from '@/api'
  9. import { InferResponseType, InferRequestType } from 'hono'
  10. import { Navbar } from '@/components/ui/navbar'
  11. import { Card } from '@/components/ui/card'
  12. import { Button } from '@/components/ui/button'
  13. import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
  14. import { Input } from '@/components/ui/input'
  15. import { useAuth } from '@/utils/auth'
  16. type Address = InferResponseType<typeof deliveryAddressClient[':id']['$get'], 200>
  17. type CreateAddressRequest = InferRequestType<typeof deliveryAddressClient.$post>['json']
  18. type UpdateAddressRequest = InferRequestType<typeof deliveryAddressClient[':id']['$put']>['json']
  19. const addressSchema = z.object({
  20. name: z.string().min(1, '请输入收货人姓名'),
  21. phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号'),
  22. province: z.number().positive('请选择省份'),
  23. city: z.number().positive('请选择城市'),
  24. district: z.number().positive('请选择区县'),
  25. address: z.string().min(1, '请输入详细地址'),
  26. isDefault: z.boolean().optional()
  27. })
  28. type AddressFormData = z.infer<typeof addressSchema>
  29. export default function AddressEditPage() {
  30. const { user } = useAuth()
  31. const queryClient = useQueryClient()
  32. const [addressId, setAddressId] = useState<number | null>(null)
  33. const [provinces, setProvinces] = useState<any[]>([])
  34. const [cities, setCities] = useState<any[]>([])
  35. const [districts, setDistricts] = useState<any[]>([])
  36. // 获取地址ID
  37. useEffect(() => {
  38. const params = Taro.getCurrentInstance().router?.params
  39. if (params?.id) {
  40. setAddressId(parseInt(params.id))
  41. }
  42. }, [])
  43. // 获取地址详情
  44. const { data: address } = useQuery({
  45. queryKey: ['address', addressId],
  46. queryFn: async () => {
  47. if (!addressId) return null
  48. const response = await deliveryAddressClient[':id'].$get({
  49. param: { id: addressId }
  50. })
  51. if (response.status !== 200) {
  52. throw new Error('获取地址失败')
  53. }
  54. return response.json()
  55. },
  56. enabled: !!addressId,
  57. })
  58. // 获取省份
  59. const { data: provinceData } = useQuery({
  60. queryKey: ['provinces'],
  61. queryFn: async () => {
  62. const response = await cityClient.$get({
  63. query: {
  64. page: 1,
  65. pageSize: 100,
  66. filters: JSON.stringify({ parentId: 0 })
  67. }
  68. })
  69. if (response.status !== 200) {
  70. throw new Error('获取省份失败')
  71. }
  72. return response.json()
  73. }
  74. })
  75. // 获取城市
  76. const fetchCities = async (provinceId: number) => {
  77. const response = await cityClient.$get({
  78. query: {
  79. page: 1,
  80. pageSize: 100,
  81. filters: JSON.stringify({ parentId: provinceId })
  82. }
  83. })
  84. if (response.status !== 200) {
  85. throw new Error('获取城市失败')
  86. }
  87. return response.json()
  88. }
  89. // 获取区县
  90. const fetchDistricts = async (cityId: number) => {
  91. const response = await cityClient.$get({
  92. query: {
  93. page: 1,
  94. pageSize: 100,
  95. filters: JSON.stringify({ parentId: cityId })
  96. }
  97. })
  98. if (response.status !== 200) {
  99. throw new Error('获取区县失败')
  100. }
  101. return response.json()
  102. }
  103. // 表单设置
  104. const form = useForm<AddressFormData>({
  105. resolver: zodResolver(addressSchema),
  106. defaultValues: {
  107. name: '',
  108. phone: '',
  109. province: 0,
  110. city: 0,
  111. district: 0,
  112. address: '',
  113. isDefault: false
  114. }
  115. })
  116. // 填充表单数据
  117. useEffect(() => {
  118. if (address) {
  119. form.reset({
  120. name: address.name,
  121. phone: address.phone,
  122. province: address.receiverProvince,
  123. city: address.receiverCity,
  124. district: address.receiverDistrict,
  125. address: address.address,
  126. isDefault: address.isDefault === 1
  127. })
  128. }
  129. }, [address, form])
  130. // 创建/更新地址
  131. const saveAddressMutation = useMutation({
  132. mutationFn: async (data: AddressFormData) => {
  133. const addressData: any = {
  134. name: data.name,
  135. phone: data.phone,
  136. receiverProvince: data.province,
  137. receiverCity: data.city,
  138. receiverDistrict: data.district,
  139. address: data.address,
  140. userId: user?.id,
  141. isDefault: data.isDefault ? 1 : 0
  142. }
  143. let response
  144. if (addressId) {
  145. response = await deliveryAddressClient[':id']['$put']({
  146. param: { id: addressId },
  147. json: addressData
  148. })
  149. } else {
  150. response = await deliveryAddressClient.$post({ json: addressData })
  151. }
  152. if (response.status !== 200 && response.status !== 201) {
  153. throw new Error('保存地址失败')
  154. }
  155. return response.json()
  156. },
  157. onSuccess: () => {
  158. queryClient.invalidateQueries({ queryKey: ['delivery-addresses'] })
  159. Taro.showToast({
  160. title: addressId ? '更新成功' : '添加成功',
  161. icon: 'success'
  162. })
  163. Taro.navigateBack()
  164. },
  165. onError: (error) => {
  166. Taro.showToast({
  167. title: error.message || '保存失败',
  168. icon: 'none'
  169. })
  170. }
  171. })
  172. // 处理省份变化
  173. const handleProvinceChange = async (provinceId: number) => {
  174. if (provinceId) {
  175. const response = await fetchCities(provinceId)
  176. setCities(response.data || [])
  177. setDistricts([])
  178. form.setValue('city', 0)
  179. form.setValue('district', 0)
  180. }
  181. }
  182. // 处理城市变化
  183. const handleCityChange = async (cityId: number) => {
  184. if (cityId) {
  185. const response = await fetchDistricts(cityId)
  186. setDistricts(response.data || [])
  187. form.setValue('district', 0)
  188. }
  189. }
  190. // 初始化省份
  191. useEffect(() => {
  192. if (provinceData?.data) {
  193. setProvinces(provinceData.data)
  194. }
  195. }, [provinceData])
  196. // 加载城市数据
  197. useEffect(() => {
  198. const provinceId = form.watch('province')
  199. if (provinceId) {
  200. handleProvinceChange(provinceId)
  201. }
  202. }, [form.watch('province')])
  203. // 加载区县数据
  204. useEffect(() => {
  205. const cityId = form.watch('city')
  206. if (cityId) {
  207. handleCityChange(cityId)
  208. }
  209. }, [form.watch('city')])
  210. const onSubmit = (data: AddressFormData) => {
  211. saveAddressMutation.mutate(data)
  212. }
  213. return (
  214. <View className="min-h-screen bg-gray-50">
  215. <Navbar
  216. title={addressId ? '编辑地址' : '添加地址'}
  217. leftIcon="i-heroicons-chevron-left-20-solid"
  218. onClickLeft={() => Taro.navigateBack()}
  219. />
  220. <ScrollView className="h-screen pt-12 pb-20">
  221. <View className="px-4 py-4">
  222. <Form {...form}>
  223. <View className="space-y-4">
  224. <Card>
  225. <View className="p-4 space-y-4">
  226. <FormField
  227. name="name"
  228. render={({ field }) => (
  229. <FormItem>
  230. <FormLabel>收货人姓名</FormLabel>
  231. <FormControl>
  232. <Input placeholder="请输入收货人姓名" {...field} />
  233. </FormControl>
  234. <FormMessage />
  235. </FormItem>
  236. )}
  237. />
  238. <FormField
  239. name="phone"
  240. render={({ field }) => (
  241. <FormItem>
  242. <FormLabel>手机号码</FormLabel>
  243. <FormControl>
  244. <Input placeholder="请输入手机号码" {...field} />
  245. </FormControl>
  246. <FormMessage />
  247. </FormItem>
  248. )}
  249. />
  250. <FormField
  251. name="province"
  252. render={({ field }) => (
  253. <FormItem>
  254. <FormLabel>省份</FormLabel>
  255. <FormControl>
  256. <picker
  257. mode="selector"
  258. range={provinces}
  259. range-key="name"
  260. value={provin