add-address.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. import PageContainerBehavior from "../../../../core/behavior/page-container.behavior";
  2. // import parseAddress from "../../../../utils/address-parse";
  3. import AddressParse from "address-parse";
  4. import tickleBehavior, {
  5. getTickleContext,
  6. } from "../../../../core/behavior/tickle.behavior";
  7. import { getAddressDetailByPatientIdMethod } from "../../request";
  8. import { areaList } from "@vant/area-data";
  9. import {
  10. addAddressMethod,
  11. deleteAddressMethod,
  12. updateAddressMethod,
  13. } from "../../request";
  14. // 将 @vant/area-data 扁平格式转为 TDesign Cascader 树形格式
  15. function convertAreaDataToTree(areaList: { province_list: Record<string, string>; city_list: Record<string, string>; county_list: Record<string, string> }) {
  16. const { province_list, city_list, county_list } = areaList;
  17. const cityMap: Record<string, any[]> = {};
  18. for (const cityCode in city_list) {
  19. const prefix = cityCode.substring(0, 2);
  20. if (!cityMap[prefix]) cityMap[prefix] = [];
  21. cityMap[prefix].push({ label: city_list[cityCode], value: cityCode, children: [] });
  22. }
  23. const countyMap: Record<string, any[]> = {};
  24. for (const countyCode in county_list) {
  25. const prefix = countyCode.substring(0, 4);
  26. if (!countyMap[prefix]) countyMap[prefix] = [];
  27. countyMap[prefix].push({ label: county_list[countyCode], value: countyCode });
  28. }
  29. const result: any[] = [];
  30. for (const provinceCode in province_list) {
  31. const children = (cityMap[provinceCode.substring(0, 2)] || []).map((city: any) => ({
  32. ...city,
  33. children: countyMap[city.value.substring(0, 4)] || [],
  34. }));
  35. if (children.length > 0) {
  36. result.push({ label: province_list[provinceCode], value: provinceCode, children });
  37. }
  38. }
  39. return result;
  40. }
  41. // module/diet/pages/delivery-address/delivery-address.ts
  42. // import parseAddress from '../../utils/smart-parse-address';
  43. Page({
  44. behaviors: [PageContainerBehavior, tickleBehavior],
  45. properties: {},
  46. data: {
  47. type: "",
  48. areaList,
  49. areaTreeData: convertAreaDataToTree(areaList),
  50. id: "",
  51. formData: {
  52. id: "",
  53. provinceCode: "",
  54. provinceName: "",
  55. cityCode: "",
  56. cityName: "",
  57. areaCode: "",
  58. areaName: "",
  59. liaison: "",
  60. phone: "",
  61. isDefault: "N",
  62. detailAddress: "",
  63. tagList: [],
  64. },
  65. region: "", // 展示在输入框的省市区字符串
  66. regionValue: [], // picker 选中的值
  67. regionPickerVisible: false,
  68. tagList: ["家", "公司", "学校"],
  69. selectedTag: "",
  70. focusName: false,
  71. focusPhone: false,
  72. focusDetail: false,
  73. smartPasteValue: "",
  74. smartPasteFocus: false,
  75. smartPastePlaceholder: "点击此处粘贴地址信息,可快速识别姓名、电话、地址",
  76. saving: false,
  77. },
  78. onClearDetail() {
  79. this.setData({ detail: "" });
  80. },
  81. onRegionConfirm(e: any) {
  82. const selectedOptions = e.detail.selectedOptions;
  83. if (!selectedOptions || selectedOptions.length < 3) return;
  84. this.setData({
  85. region:
  86. selectedOptions[0].label +
  87. ", " +
  88. selectedOptions[1].label +
  89. "," +
  90. selectedOptions[2].label,
  91. regionValue: e.detail.value,
  92. regionPickerVisible: false,
  93. "formData.provinceCode": selectedOptions[0].value,
  94. "formData.provinceName": selectedOptions[0].label,
  95. "formData.cityCode": selectedOptions[1].value,
  96. "formData.cityName": selectedOptions[1].label,
  97. "formData.areaCode": selectedOptions[2].value,
  98. "formData.areaName": selectedOptions[2].label,
  99. });
  100. },
  101. onRegionCancel(e: any) {
  102. this.setData({
  103. regionPickerVisible: false,
  104. });
  105. },
  106. // 输入框双向绑定
  107. onInput(e) {
  108. const { field } = e.currentTarget.dataset;
  109. // 这里 field 必须是 'name'、'phone'、'detail' 之一
  110. this.setData({ [field]: e.detail.value });
  111. },
  112. // 收货人
  113. onFocusInput() {
  114. this.setData({ focusName: true });
  115. },
  116. onBlurInput() {
  117. this.setData({ focusName: false });
  118. },
  119. // 手机号
  120. onFocusPhone() {
  121. this.setData({ focusPhone: true });
  122. },
  123. onBlurPhone() {
  124. this.setData({ focusPhone: false });
  125. },
  126. // 详细地址
  127. onFocusDetail() {
  128. this.setData({ focusDetail: true });
  129. },
  130. onBlurDetail() {
  131. this.setData({ focusDetail: false });
  132. },
  133. // 地区选择
  134. onShowRegionPicker() {
  135. this.setData({ regionPickerVisible: true });
  136. },
  137. // 标签选择
  138. onTagSelect(e) {
  139. const tag = e.currentTarget.dataset.tag;
  140. this.setData({ selectedTag: tag });
  141. },
  142. // 默认地址开关
  143. onDefaultChange(e) {
  144. if (e.detail.value) {
  145. this.setData({ "formData.isDefault": "Y" });
  146. } else {
  147. this.setData({ "formData.isDefault": "N" });
  148. }
  149. },
  150. // 添加地址
  151. async onAddress(params: any) {
  152. const res = await addAddressMethod(params);
  153. wx.showToast({ title: "添加成功", icon: "success" });
  154. },
  155. // 更新地址
  156. async onUpdate(params: any) {
  157. const res = await updateAddressMethod(params);
  158. wx.showToast({ title: "更新成功", icon: "success" });
  159. },
  160. // 保存
  161. async onSave() {
  162. if (this.data.saving) return; // 防重复
  163. try {
  164. const { liaison, phone, detailAddress } = this.data.formData;
  165. if (!liaison) {
  166. wx.showToast({ title: "收货人为空", icon: "none" });
  167. return;
  168. }
  169. if (!phone) {
  170. wx.showToast({ title: "手机号为空", icon: "none" });
  171. return;
  172. }
  173. // 手机号校验
  174. if (!/^1\d{10}$/.test(phone)) {
  175. wx.showToast({ title: "您填写的手机号有误", icon: "none" });
  176. return;
  177. }
  178. if (!this.data.region) {
  179. wx.showToast({ title: "所在地区为空", icon: "none" });
  180. return;
  181. }
  182. if (!detailAddress) {
  183. wx.showToast({ title: "详细地址为空", icon: "none" });
  184. return;
  185. }
  186. this.setData({
  187. "formData.tagList": [this.data.selectedTag],
  188. });
  189. this.setData({ saving: true });
  190. if (this.data.id) {
  191. this.data.formData.id = this.data.id;
  192. await this.onUpdate(this.data.formData);
  193. } else {
  194. await this.onAddress(this.data.formData);
  195. }
  196. this.setData({ saving: false });
  197. // 返回上一页(避免 redirectTo 造成 manage-address 重复入栈)
  198. const pages = getCurrentPages();
  199. if (pages.length > 1) {
  200. wx.navigateBack();
  201. } else {
  202. // 极端情况下没有上一页(比如被 reLaunch 打开),兜底回地址管理
  203. wx.redirectTo({
  204. url: "/module/article/pages/manage-address/manage-address",
  205. });
  206. }
  207. // 保存成功后可自动跳转或提示
  208. } catch (error: any) {
  209. wx.showToast({ title: error.errMsg, icon: "none" });
  210. this.setData({ saving: false }); // 无论成功失败都重置
  211. }
  212. },
  213. // 输入框聚焦
  214. onSmartPasteFocus() {
  215. this.setData({
  216. smartPasteFocus: true,
  217. smartPastePlaceholder:
  218. "粘贴或输入地址信息,如:张三,1801889955,上海市浦东新区浦东大道100号,可自动识别并填充文本",
  219. });
  220. },
  221. // 输入框失焦
  222. onSmartPasteBlur() {
  223. if (this.data.smartPasteValue) {
  224. this.setData({
  225. smartPasteFocus: true,
  226. });
  227. } else {
  228. this.setData({
  229. smartPasteFocus: false,
  230. smartPastePlaceholder:
  231. "点击此处粘贴地址信息,可快速识别姓名、电话、地址",
  232. });
  233. }
  234. },
  235. // 输入内容
  236. onSmartPasteInput(e: any) {
  237. this.setData({ smartPasteValue: e.detail.value });
  238. },
  239. // 清空
  240. onClearSmartPaste() {
  241. this.setData({ smartPasteValue: "" });
  242. },
  243. // 获取省市区编码
  244. getCodeByName(list: any, name: any) {
  245. for (const code in list) {
  246. if (list[code] === name) {
  247. return code;
  248. }
  249. }
  250. return "";
  251. },
  252. // 删除地址
  253. async onDelete() {
  254. const that = this;
  255. wx.showModal({
  256. title: "确定删除地址?",
  257. content: "删除后地址不可恢复",
  258. confirmColor: "#1D6FF6",
  259. cancelColor: "#999",
  260. cancelText: "取消",
  261. confirmText: "删除",
  262. success: async (res) => {
  263. if (res.confirm) {
  264. const res = await deleteAddressMethod(that.data.id);
  265. wx.showToast({ title: "删除成功", icon: "success" });
  266. wx.navigateBack();
  267. }
  268. },
  269. });
  270. },
  271. // 识别
  272. async onRecognizeSmartPaste() {
  273. let value = this.data.smartPasteValue.trim();
  274. if (!value) return;
  275. // 1. 统一分隔符,兼容各种输入
  276. value = value.replace(/[\n,、;;::]/g, ",").replace(/\s+/g, ",");
  277. // 2. 提取手机号
  278. const phoneMatch = value.match(/1\\d{10}/);
  279. let phone = phoneMatch ? phoneMatch[0] : "";
  280. if (phone) value = value.replace(phone, ",");
  281. // 3. 提取姓名(2-4个汉字,通常在最前面)
  282. let name = "";
  283. let nameMatch = value.match(/^[\\u4e00-\\u9fa5]{2,4}/);
  284. if (nameMatch) {
  285. name = nameMatch[0];
  286. value = value.replace(name, ",");
  287. }
  288. // 5. 用智能库进一步解析省市区
  289. let [result] = AddressParse.parse(this.data.smartPasteValue);
  290. result = {
  291. ...result,
  292. name: result.name,
  293. phone: result.phone || result.mobile,
  294. detailAddress: result.details,
  295. county: result.area,
  296. provinceCode: result.provinceCode,
  297. cityCode: result.cityCode,
  298. areaCode: result.areaCode,
  299. };
  300. // 6. 校验
  301. if (!result.name || !result.phone) {
  302. wx.showToast({ title: "请按照提示输入正确的地址信息", icon: "none" });
  303. return;
  304. }
  305. if (!/^1\\d{10}$/.test(result.phone)) {
  306. wx.showToast({ title: "请按照提示输入正确的地址信息", icon: "none" });
  307. }
  308. // 7. 省市区识别
  309. const { province, city, county } = result;
  310. const { province_list, city_list, county_list } = this.data.areaList;
  311. if (province && city) {
  312. const provinceCode = this.getCodeByName(province_list, province);
  313. const cityCode = this.getCodeByName(city_list, city);
  314. const areaCode = this.getCodeByName(county_list, county);
  315. this.setData({
  316. "formData.provinceName": province,
  317. "formData.cityName": city,
  318. "formData.areaName": county || "",
  319. "formData.provinceCode": provinceCode,
  320. "formData.cityCode": cityCode,
  321. "formData.areaCode": areaCode,
  322. "formData.detailAddress": result.details,
  323. region: `${province},${city}${county ? "," + county : ""}`,
  324. });
  325. } else {
  326. wx.showToast({ title: "请按照提示输入正确的地址信息", icon: "none" });
  327. }
  328. // 其他字段(如姓名、手机号、详细地址)可以单独 setData,不受省市影响
  329. this.setData({
  330. "formData.liaison": result.name,
  331. "formData.phone": result.phone || result.mobile,
  332. });
  333. wx.showToast({ title: "已识别", icon: "success" });
  334. },
  335. // 获取地址详情
  336. async getAddressDetail(id: string) {
  337. try {
  338. const res = await getAddressDetailByPatientIdMethod(id);
  339. if (res && res.data) {
  340. this.setData({
  341. "formData.isDefault": res.data.isDefault,
  342. "formData.liaison": res.data.liaison,
  343. "formData.phone": res.data.phone,
  344. "formData.detailAddress": res.data.detailAddress,
  345. "formData.provinceName": res.data.provinceName,
  346. "formData.cityName": res.data.cityName,
  347. "formData.areaName": res.data.areaName,
  348. "formData.provinceCode": res.data.provinceCode,
  349. "formData.cityCode": res.data.cityCode,
  350. "formData.areaCode": res.data.areaCode,
  351. region: `${res.data.provinceName} ${res.data.cityName} ${res.data.areaName}`,
  352. });
  353. // 标签
  354. this.setData({
  355. "formData.tagList": res.data.tagList,
  356. });
  357. this.setData({
  358. selectedTag: res.data.tagList[0] || "",
  359. });
  360. }
  361. } catch (error: any) {
  362. getTickleContext.call(this).showWarnMessage(error.errMsg);
  363. }
  364. },
  365. onLoad(options) {
  366. if (options.id) {
  367. this.setData({ id: options.id });
  368. this.getAddressDetail(options.id);
  369. }
  370. if (options.type) {
  371. this.setData({ type: options.type });
  372. }
  373. if (options.imported) {
  374. const imported = JSON.parse(decodeURIComponent(options.imported));
  375. const { provinceName, cityName, areaName } = imported;
  376. const { province_list, city_list, county_list } = this.data.areaList;
  377. if (provinceName && cityName) {
  378. const provinceCode = this.getCodeByName(province_list, provinceName);
  379. const cityCode = this.getCodeByName(city_list, cityName);
  380. const areaCode = this.getCodeByName(county_list, areaName);
  381. this.setData({
  382. "formData.isDefault": imported.isDefault || "N",
  383. "formData.liaison": imported.liaison,
  384. "formData.phone": imported.phone,
  385. "formData.detailAddress": imported.detailAddress,
  386. "formData.provinceName": imported.provinceName,
  387. "formData.cityName": imported.cityName,
  388. "formData.areaName": imported.areaName,
  389. "formData.provinceCode": provinceCode,
  390. "formData.cityCode": cityCode,
  391. "formData.areaCode": areaCode,
  392. region: imported.region,
  393. });
  394. this.setData({
  395. selectedTag: "",
  396. type: "imported",
  397. });
  398. }
  399. }
  400. },
  401. });