edit.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import { useLocalSearchParams, useRouter } from 'expo-router';
  2. import React, { useCallback, useEffect, useState } from 'react';
  3. import {
  4. ActivityIndicator,
  5. Alert,
  6. ImageBackground,
  7. ScrollView,
  8. StatusBar,
  9. StyleSheet,
  10. Switch,
  11. Text,
  12. TextInput,
  13. TouchableOpacity,
  14. View
  15. } from 'react-native';
  16. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  17. import { RegionPicker } from '@/components/RegionPicker';
  18. import { Images } from '@/constants/images';
  19. import { addAddress, Address, getAddressList, updateAddress } from '@/services/address';
  20. export default function AddressEditScreen() {
  21. const { id } = useLocalSearchParams<{ id?: string }>();
  22. const router = useRouter();
  23. const insets = useSafeAreaInsets();
  24. const [loading, setLoading] = useState(!!id);
  25. const [saving, setSaving] = useState(false);
  26. const [contactName, setContactName] = useState('');
  27. const [contactNo, setContactNo] = useState('');
  28. const [province, setProvince] = useState('');
  29. const [city, setCity] = useState('');
  30. const [district, setDistrict] = useState('');
  31. const [address, setAddress] = useState('');
  32. const [isDefault, setIsDefault] = useState(false);
  33. const [regionVisible, setRegionVisible] = useState(false);
  34. const handleRegionSelect = (p: string, c: string, d: string) => {
  35. setProvince(p);
  36. setCity(c);
  37. setDistrict(d);
  38. };
  39. const loadData = useCallback(async () => {
  40. if (!id) return;
  41. setLoading(true);
  42. try {
  43. const list = await getAddressList();
  44. const item = list.find((a: Address) => a.id === id);
  45. if (item) {
  46. setContactName(item.contactName);
  47. setContactNo(item.contactNo);
  48. setProvince(item.province);
  49. setCity(item.city);
  50. setDistrict(item.district || '');
  51. setAddress(item.address);
  52. setIsDefault(item.defaultFlag === 1);
  53. }
  54. } catch (error) {
  55. console.error('加载地址失败:', error);
  56. }
  57. setLoading(false);
  58. }, [id]);
  59. useEffect(() => {
  60. loadData();
  61. }, [loadData]);
  62. const handleSave = async () => {
  63. if (!contactName.trim()) {
  64. Alert.alert('提示', '请输入收货人姓名');
  65. return;
  66. }
  67. if (!contactNo.trim()) {
  68. Alert.alert('提示', '请输入手机号码');
  69. return;
  70. }
  71. if (!province.trim() || !city.trim()) {
  72. Alert.alert('提示', '请输入省市信息');
  73. return;
  74. }
  75. if (!address.trim()) {
  76. Alert.alert('提示', '请输入详细地址');
  77. return;
  78. }
  79. setSaving(true);
  80. try {
  81. const params = {
  82. contactName: contactName.trim(),
  83. contactNo: contactNo.trim(),
  84. province: province.trim(),
  85. city: city.trim(),
  86. district: district.trim(),
  87. address: address.trim(),
  88. defaultFlag: isDefault ? 1 : 0,
  89. };
  90. let success = false;
  91. if (id) {
  92. success = await updateAddress({ id, ...params } as Address);
  93. } else {
  94. success = await addAddress(params);
  95. }
  96. if (success) {
  97. Alert.alert('成功', id ? '地址已更新' : '地址已添加', [
  98. { text: '确定', onPress: () => router.back() },
  99. ]);
  100. }
  101. } catch (error) {
  102. console.error('保存地址失败:', error);
  103. Alert.alert('错误', '保存失败');
  104. }
  105. setSaving(false);
  106. };
  107. const goBack = () => {
  108. router.back();
  109. };
  110. if (loading) {
  111. return (
  112. <View style={styles.loadingContainer}>
  113. <ActivityIndicator size="large" color="#fff" />
  114. </View>
  115. );
  116. }
  117. return (
  118. <View style={styles.container}>
  119. <StatusBar barStyle="light-content" />
  120. <ImageBackground
  121. source={{ uri: Images.mine.kaixinMineBg }}
  122. style={styles.background}
  123. resizeMode="cover"
  124. >
  125. {/* 顶部导航 */}
  126. <View style={[styles.header, { paddingTop: insets.top }]}>
  127. <TouchableOpacity style={styles.backBtn} onPress={goBack}>
  128. <Text style={styles.backText}>←</Text>
  129. </TouchableOpacity>
  130. <Text style={styles.headerTitle}>{id ? '编辑地址' : '新增地址'}</Text>
  131. <View style={styles.placeholder} />
  132. </View>
  133. <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
  134. <ImageBackground
  135. source={{ uri: Images.common.itemBg }}
  136. style={styles.formBg}
  137. resizeMode="stretch"
  138. >
  139. <View style={styles.form}>
  140. <View style={styles.formItem}>
  141. <Text style={styles.label}>收货人</Text>
  142. <TextInput
  143. style={styles.input}
  144. value={contactName}
  145. onChangeText={setContactName}
  146. placeholder="请输入收货人姓名"
  147. placeholderTextColor="#999"
  148. />
  149. </View>
  150. <View style={styles.formItem}>
  151. <Text style={styles.label}>手机号码</Text>
  152. <TextInput
  153. style={styles.input}
  154. value={contactNo}
  155. onChangeText={setContactNo}
  156. placeholder="请输入手机号码"
  157. placeholderTextColor="#999"
  158. keyboardType="phone-pad"
  159. />
  160. </View>
  161. <View style={styles.formItem}>
  162. <Text style={styles.label}>所在地区</Text>
  163. <TouchableOpacity
  164. style={styles.selector}
  165. onPress={() => setRegionVisible(true)}
  166. >
  167. <Text style={[styles.selectorText, !province && styles.placeholderText]}>
  168. {province ? `${province} ${city} ${district}` : '请选择省/市/区'}
  169. </Text>
  170. </TouchableOpacity>
  171. </View>
  172. <View style={styles.formItem}>
  173. <Text style={styles.label}>详细地址</Text>
  174. <TextInput
  175. style={[styles.input, styles.textArea]}
  176. value={address}
  177. onChangeText={setAddress}
  178. placeholder="请输入详细地址"
  179. placeholderTextColor="#999"
  180. multiline
  181. numberOfLines={3}
  182. />
  183. </View>
  184. <View style={styles.switchItem}>
  185. <Text style={styles.switchLabel}>设为默认地址</Text>
  186. <Switch value={isDefault} onValueChange={setIsDefault} />
  187. </View>
  188. </View>
  189. </ImageBackground>
  190. <View style={{ height: 100 }} />
  191. </ScrollView>
  192. {/* 底部保存按钮 */}
  193. <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
  194. <TouchableOpacity
  195. style={[styles.saveBtn, saving && styles.saveBtnDisabled]}
  196. onPress={handleSave}
  197. disabled={saving}
  198. activeOpacity={0.8}
  199. >
  200. <ImageBackground
  201. source={{ uri: Images.common.loginBtn }}
  202. style={styles.saveBtnBg}
  203. resizeMode="stretch"
  204. >
  205. <Text style={styles.saveBtnText}>{saving ? '保存中...' : '保存'}</Text>
  206. </ImageBackground>
  207. </TouchableOpacity>
  208. </View>
  209. </ImageBackground>
  210. <RegionPicker
  211. visible={regionVisible}
  212. onClose={() => setRegionVisible(false)}
  213. onSelect={handleRegionSelect}
  214. />
  215. </View>
  216. );
  217. }
  218. const styles = StyleSheet.create({
  219. container: {
  220. flex: 1,
  221. backgroundColor: '#1a1a2e',
  222. },
  223. background: {
  224. flex: 1,
  225. },
  226. loadingContainer: {
  227. flex: 1,
  228. backgroundColor: '#1a1a2e',
  229. justifyContent: 'center',
  230. alignItems: 'center',
  231. },
  232. header: {
  233. flexDirection: 'row',
  234. alignItems: 'center',
  235. justifyContent: 'space-between',
  236. paddingHorizontal: 15,
  237. paddingBottom: 10,
  238. },
  239. backBtn: {
  240. width: 40,
  241. height: 40,
  242. justifyContent: 'center',
  243. alignItems: 'center',
  244. },
  245. backText: {
  246. color: '#fff',
  247. fontSize: 24,
  248. },
  249. headerTitle: {
  250. color: '#fff',
  251. fontSize: 18,
  252. fontWeight: '600',
  253. },
  254. placeholder: {
  255. width: 40,
  256. },
  257. scrollView: {
  258. flex: 1,
  259. },
  260. formBg: {
  261. margin: 15,
  262. padding: 15,
  263. borderRadius: 12,
  264. overflow: 'hidden',
  265. },
  266. form: {},
  267. formItem: {
  268. marginBottom: 15,
  269. },
  270. label: {
  271. color: '#333',
  272. fontSize: 14,
  273. marginBottom: 8,
  274. },
  275. input: {
  276. backgroundColor: 'rgba(255,255,255,0.8)',
  277. borderRadius: 8,
  278. paddingHorizontal: 12,
  279. paddingVertical: 12,
  280. color: '#333',
  281. fontSize: 14,
  282. },
  283. selector: {
  284. backgroundColor: 'rgba(255,255,255,0.8)',
  285. borderRadius: 8,
  286. paddingHorizontal: 12,
  287. paddingVertical: 12,
  288. },
  289. selectorText: {
  290. fontSize: 14,
  291. color: '#333',
  292. },
  293. placeholderText: {
  294. color: '#999',
  295. },
  296. textArea: {
  297. height: 80,
  298. textAlignVertical: 'top',
  299. },
  300. switchItem: {
  301. flexDirection: 'row',
  302. justifyContent: 'space-between',
  303. alignItems: 'center',
  304. paddingTop: 10,
  305. },
  306. switchLabel: {
  307. color: '#333',
  308. fontSize: 14,
  309. },
  310. bottomBar: {
  311. paddingHorizontal: 15,
  312. paddingTop: 10,
  313. },
  314. saveBtn: {
  315. height: 50,
  316. overflow: 'hidden',
  317. },
  318. saveBtnBg: {
  319. width: '100%',
  320. height: '100%',
  321. justifyContent: 'center',
  322. alignItems: 'center',
  323. },
  324. saveBtnDisabled: {
  325. opacity: 0.6,
  326. },
  327. saveBtnText: {
  328. color: '#fff',
  329. fontSize: 16,
  330. fontWeight: '600',
  331. },
  332. });