爱简收旧衣按件回收前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

605 lines
18 KiB

2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
  1. <template>
  2. <view class="container" :style="{paddingTop: (statusBarHeight + 88) + 'rpx'}">
  3. <!-- 顶部导航 -->
  4. <view class="nav-bar" :style="{height: (statusBarHeight + 88) + 'rpx', paddingTop: statusBarHeight + 'px'}">
  5. <view class="back" @tap="goBack">
  6. <uni-icons type="left" size="20"></uni-icons>
  7. </view>
  8. <text class="title">选择寄件地址</text>
  9. </view>
  10. <!-- 地址列表 -->
  11. <view class="address-list">
  12. <view
  13. class="address-item"
  14. @click="selectAddress(item)"
  15. v-for="(item, index) in addressList"
  16. :key="index"
  17. >
  18. <view class="address-header">
  19. <text class="name">{{item.name}}</text>
  20. <text class="phone">{{item.phone}}</text>
  21. <text v-if="item.defaultFlag === 'Y'" class="default-tag">默认</text>
  22. </view>
  23. <view class="address-text">{{item.address}} {{item.addressDetails}}</view>
  24. <view class="dashed-line"></view>
  25. <view class="address-actions">
  26. <view class="action" @tap="setDefault(item.id)">
  27. <view class="circle" :class="{ active: item.defaultFlag === 'Y' }">
  28. <text v-if="item.defaultFlag === 'Y'" class="check"></text>
  29. </view>
  30. <text :class="{ 'active-text': item.defaultFlag === 'Y' }">默认地址</text>
  31. </view>
  32. <view class="action" @tap="editAddress(item)">
  33. <uni-icons type="compose" size="22" color="#bbb" />
  34. <text>编辑</text>
  35. </view>
  36. <view class="action" @tap="deleteAddress(item.id)">
  37. <uni-icons type="trash" size="22" color="#bbb" />
  38. <text>删除</text>
  39. </view>
  40. </view>
  41. </view>
  42. </view>
  43. <!-- 新建地址按钮 -->
  44. <view class="bottom-bar">
  45. <button class="main-btn" @tap="goToAddAddress">新建地址</button>
  46. </view>
  47. <!-- 遮罩 -->
  48. <view v-if="showAddressModal" class="modal-mask" @tap="closeAddressModal"></view>
  49. <!-- 新建地址弹窗 -->
  50. <view v-if="showAddressModal" class="address-modal">
  51. <view class="modal-header">
  52. <text class="close-btn" @tap="closeAddressModal">关闭</text>
  53. <text class="modal-title">{{form.id ? '编辑地址' : '新建地址'}}</text>
  54. </view>
  55. <view class="modal-form">
  56. <view class="form-item">
  57. <text class="label">联系人</text>
  58. <input class="input" v-model="form.name" placeholder="请输入" />
  59. </view>
  60. <view class="form-item">
  61. <text class="label">手机号</text>
  62. <input class="input" v-model="form.phone" placeholder="请输入" />
  63. </view>
  64. <view class="form-item" @tap="showRegionPicker = true">
  65. <text class="label">所在地区</text>
  66. <view class="input region-input">
  67. <text :class="{placeholder: !form.address}">
  68. {{ form.address || '选择省市区街道' }}
  69. </text>
  70. <text class="arrow">></text>
  71. </view>
  72. </view>
  73. <view class="form-item">
  74. <text class="label">详细地址</text>
  75. <input class="input" v-model="form.addressDetails" placeholder="小区楼栋、门牌号、村等" />
  76. </view>
  77. </view>
  78. <button class="main-btn" @tap="saveAddress">保存</button>
  79. </view>
  80. <!-- 地区选择器弹窗 -->
  81. <view v-if="showRegionPicker" class="region-modal">
  82. <view class="modal-header">
  83. <text class="close-btn" @tap="showRegionPicker = false">关闭</text>
  84. <text class="modal-title">所在地区</text>
  85. </view>
  86. <view class="region-picker">
  87. <picker-view style="height:300px;" :value="regionIndex" @change="onRegionChange">
  88. <picker-view-column >
  89. <view v-for="(item,index) in provinces" :key="index" class="item">{{item.name}}</view>
  90. </picker-view-column>
  91. <picker-view-column>
  92. <view v-for="(item,index) in cities" :key="index" class="item">{{item.name}}</view>
  93. </picker-view-column>
  94. <picker-view-column>
  95. <view v-for="(item,index) in districts" :key="index" class="item">{{item.name}}</view>
  96. </picker-view-column>
  97. </picker-view>
  98. </view>
  99. <button class="main-btn" @tap="confirmRegion">确认</button>
  100. </view>
  101. </view>
  102. </template>
  103. <script>
  104. import regionData from '@/pages/subcomponent/region-data.js'
  105. import pullRefreshMixin from '@/pages/mixins/pullRefreshMixin.js'
  106. export default {
  107. mixins: [pullRefreshMixin],
  108. data() {
  109. return {
  110. statusBarHeight: 0,
  111. addressList: [
  112. {
  113. name: '郑文锦',
  114. phone: '18108341643',
  115. address: '海南省海口市秀英区秀英街道5单元183室',
  116. defaultFlag: 'Y'
  117. },
  118. {
  119. name: '周俊',
  120. phone: '13293992217',
  121. address: '贵州省遵义市道真仡佬族苗族自治县洛龙镇5幢172室',
  122. defaultFlag: 'N'
  123. },
  124. {
  125. name: '何炜',
  126. phone: '18108341643',
  127. address: '新疆维吾尔自治区乌鲁木齐市沙依巴克区仓房沟片区街道4单元50室',
  128. defaultFlag: 'N'
  129. },
  130. {
  131. name: '赵晓艳',
  132. phone: '15022123314',
  133. address: '海南省海口市秀英区秀英街道5单元183室',
  134. defaultFlag: 'N'
  135. },
  136. {
  137. name: '冯云',
  138. phone: '15731435825',
  139. address: '甘肃省兰州市城关区滑源路街道13幢199室',
  140. defaultFlag: 'N'
  141. },
  142. {
  143. name: '钱皓皓',
  144. phone: '18734717201',
  145. address: '西藏自治区拉萨市堆龙德庆县东嘎镇7单元94室',
  146. defaultFlag: 'N'
  147. }
  148. ],
  149. batchMode: false,
  150. selectedIndexes: [],
  151. showAddressModal: false,
  152. showRegionPicker: false,
  153. form: {
  154. name: '',
  155. phone: '',
  156. region: [],
  157. address: '',
  158. addressDetails: ''
  159. },
  160. provinces: regionData,
  161. cities: [],
  162. districts: [],
  163. regionIndex: [0, 0, 0],
  164. mode: ''
  165. }
  166. },
  167. watch: {
  168. regionIndex: {
  169. handler(val) {
  170. let pIdx = val[0] < this.provinces.length ? val[0] : 0
  171. let cIdx = val[1] < (this.provinces[pIdx]?.children?.length || 0) ? val[1] : 0
  172. this.cities = this.provinces[pIdx]?.children || []
  173. this.districts = this.cities[cIdx]?.children || []
  174. },
  175. immediate: true
  176. }
  177. },
  178. onPullDownRefresh() {
  179. this.getAddressList(() => {
  180. uni.stopPullDownRefresh();
  181. });
  182. },
  183. onLoad(options) {
  184. this.statusBarHeight = uni.getSystemInfoSync().statusBarHeight
  185. this.cities = this.provinces[0]?.children || []
  186. this.districts = this.cities[0]?.children || []
  187. this.regionIndex = [0, 0, 0]
  188. this.getAddressList();
  189. this.mode = options.mode || ''
  190. },
  191. methods: {
  192. async onRefresh() {
  193. // 模拟刷新数据
  194. await new Promise(resolve => setTimeout(resolve, 1000))
  195. uni.stopPullRefresh()
  196. },
  197. getAddressList(callback) {
  198. this.$api('getAddressList', {}, res => {
  199. if (res && res.result && res.result.records) {
  200. this.addressList = res.result.records;
  201. }
  202. if (typeof callback === 'function') callback();
  203. });
  204. },
  205. goBack() {
  206. uni.navigateBack()
  207. },
  208. startBatchDelete() {
  209. this.batchMode = true
  210. this.selectedIndexes = []
  211. },
  212. cancelBatchDelete() {
  213. this.batchMode = false
  214. this.selectedIndexes = []
  215. },
  216. selectItem(index) {
  217. if (!this.batchMode) return
  218. const idx = this.selectedIndexes.indexOf(index)
  219. if (idx > -1) {
  220. this.selectedIndexes.splice(idx, 1)
  221. } else {
  222. this.selectedIndexes.push(index)
  223. }
  224. },
  225. confirmDelete() {
  226. if (this.selectedIndexes.length === 0) return
  227. uni.showModal({
  228. title: '提示',
  229. content: '确定要删除选中的地址吗?',
  230. success: (res) => {
  231. if (res.confirm) {
  232. // 从后往前删除,避免索引变化的问题
  233. this.selectedIndexes.sort((a, b) => b - a).forEach(index => {
  234. this.addressList.splice(index, 1)
  235. })
  236. this.batchMode = false
  237. this.selectedIndexes = []
  238. }
  239. }
  240. })
  241. },
  242. editAddress(item) {
  243. this.showAddressModal = true
  244. this.form = {
  245. id: item.id,
  246. name: item.name,
  247. phone: item.phone,
  248. region: [],
  249. address: item.address,
  250. addressDetails: item.addressDetails,
  251. defaultFlag: item.defaultFlag
  252. }
  253. this.regionIndex = [0, 0, 0]
  254. this.cities = this.provinces[0]?.children || []
  255. this.districts = this.cities[0]?.children || []
  256. },
  257. goToAddAddress() {
  258. this.showAddressModal = true
  259. this.form = {
  260. id: undefined,
  261. name: '',
  262. phone: '',
  263. region: [],
  264. address: '',
  265. addressDetails: ''
  266. }
  267. this.regionIndex = [0, 0, 0]
  268. this.cities = this.provinces[0]?.children || []
  269. this.districts = this.cities[0]?.children || []
  270. },
  271. selectAddress(address) {
  272. if (this.mode === 'select') {
  273. uni.$emit('addressSelected', {
  274. id: address.id,
  275. name: address.name,
  276. phone: address.phone,
  277. address: address.address,
  278. addressDetails: address.addressDetails
  279. })
  280. uni.navigateBack()
  281. }
  282. },
  283. closeAddressModal() {
  284. this.showAddressModal = false
  285. },
  286. saveAddress() {
  287. if (!this.form.name) return uni.showToast({ title: '请输入联系人', icon: 'none' })
  288. if (!this.form.phone) return uni.showToast({ title: '请输入手机号', icon: 'none' })
  289. if (!this.form.address) return uni.showToast({ title: '请选择地区', icon: 'none' })
  290. if (!this.form.addressDetails) return uni.showToast({ title: '请输入详细地址', icon: 'none' })
  291. // 判断手机号是否合法
  292. if (!/^1[3-9]\d{9}$/.test(this.form.phone)) return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
  293. const params = {
  294. name: this.form.name,
  295. phone: this.form.phone,
  296. address: this.form.address,
  297. addressDetails: this.form.addressDetails,
  298. defaultFlag: this.form.defaultFlag
  299. }
  300. if (this.form.id) params.id = this.form.id
  301. this.$api('saveOrUpdateAddress', params, (res) => {
  302. if(res.code == 200){
  303. uni.showToast({ title: '保存成功', icon: 'success' })
  304. this.closeAddressModal()
  305. // 保存成功后刷新地址列表
  306. this.getAddressList()
  307. }
  308. })
  309. },
  310. onRegionChange(e) {
  311. let [pIdx, cIdx, dIdx] = e.detail.value
  312. if (pIdx !== this.regionIndex[0]) {
  313. cIdx = 0
  314. dIdx = 0
  315. } else if (cIdx !== this.regionIndex[1]) {
  316. dIdx = 0
  317. }
  318. this.regionIndex = [pIdx, cIdx, dIdx]
  319. },
  320. confirmRegion() {
  321. const province = this.provinces[this.regionIndex[0]]
  322. const city = this.cities[this.regionIndex[1]]
  323. const district = this.districts[this.regionIndex[2]]
  324. this.form.region = [
  325. province?.code || '',
  326. city?.code || '',
  327. district?.code || ''
  328. ]
  329. this.form.address = [
  330. province?.name || '',
  331. city?.name || '',
  332. district?.name || ''
  333. ].filter(Boolean).join(' ')
  334. this.showRegionPicker = false
  335. },
  336. setDefault(id) {
  337. // 找到当前地址项
  338. const address = this.addressList.find(item => item.id === id)
  339. // 如果已经是默认地址,则取消默认
  340. const defaultFlag = address.defaultFlag === 'Y' ? 'N' : 'Y'
  341. this.$api('updateDefaultAddress', {
  342. id: id
  343. }, (res) => {
  344. if(res.code == 200) {
  345. uni.showToast({
  346. title: defaultFlag === 'Y' ? '设置成功' : '已取消默认',
  347. icon: 'success'
  348. })
  349. // 设置成功后刷新地址列表
  350. this.getAddressList()
  351. }
  352. })
  353. },
  354. deleteAddress(id) {
  355. uni.showModal({
  356. title: '提示',
  357. content: '确定要删除该地址吗?',
  358. success: (res) => {
  359. if (res.confirm) {
  360. this.$api('deleteAddress', {id:id}, res => {
  361. if(res.code == 200) {
  362. uni.showToast({
  363. title: '删除成功',
  364. icon: 'success'
  365. })
  366. // 删除成功后刷新地址列表
  367. this.getAddressList()
  368. }
  369. })
  370. }
  371. }
  372. })
  373. },
  374. initRegionPicker() {
  375. if (this.form.region.length > 0) {
  376. const [provinceCode, cityCode, districtCode] = this.form.region
  377. const provinceIndex = this.provinces.findIndex(p => p.code === provinceCode)
  378. if (provinceIndex > -1) {
  379. this.cities = this.provinces[provinceIndex].children || []
  380. if (this.cities.length === 0) {
  381. this.cities = [{ code: '', name: '请选择' }]
  382. }
  383. const cityIndex = this.cities.findIndex(c => c.code === cityCode)
  384. if (cityIndex > -1) {
  385. this.districts = this.cities[cityIndex].children || []
  386. if (this.districts.length === 0) {
  387. this.districts = [{ code: '', name: '请选择' }]
  388. }
  389. const districtIndex = this.districts.findIndex(d => d.code === districtCode)
  390. this.regionIndex = [
  391. provinceIndex,
  392. cityIndex,
  393. districtIndex > -1 ? districtIndex : 0
  394. ]
  395. return
  396. }
  397. }
  398. }
  399. this.cities = this.provinces[0]?.children || []
  400. if (this.cities.length === 0) {
  401. this.cities = [{ code: '', name: '请选择' }]
  402. }
  403. this.districts = this.cities[0]?.children || []
  404. if (this.districts.length === 0) {
  405. this.districts = [{ code: '', name: '请选择' }]
  406. }
  407. this.regionIndex = [0, 0, 0]
  408. }
  409. },
  410. }
  411. </script>
  412. <style lang="scss" scoped>
  413. .container {
  414. min-height: 100vh;
  415. background: #f8f8f8;
  416. padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
  417. }
  418. .nav-bar {
  419. display: flex;
  420. align-items: center;
  421. background: #fff;
  422. padding: 0 30rpx;
  423. position: fixed;
  424. top: 0;
  425. left: 0;
  426. right: 0;
  427. z-index: 999;
  428. .back {
  429. padding: 20rpx;
  430. margin-left: -20rpx;
  431. }
  432. .title {
  433. flex: 1;
  434. text-align: center;
  435. font-size: 34rpx;
  436. font-weight: 500;
  437. color: #222;
  438. }
  439. .right-btns {
  440. display: flex;
  441. align-items: center;
  442. gap: 30rpx;
  443. .more, .target {
  444. font-size: 40rpx;
  445. color: #333;
  446. }
  447. }
  448. }
  449. .address-list {
  450. padding: 32rpx;
  451. margin-top: calc(38rpx + var(--status-bar-height));
  452. height: calc(100vh - 88rpx - var(--status-bar-height) - 140rpx - env(safe-area-inset-bottom));
  453. box-sizing: border-box;
  454. overflow-y: auto;
  455. -webkit-overflow-scrolling: touch;
  456. .address-item {
  457. background: #fff;
  458. border-radius: 24rpx;
  459. margin-bottom: 24rpx;
  460. padding: 32rpx 28rpx 0 28rpx;
  461. box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.04);
  462. .address-header {
  463. display: flex;
  464. align-items: center;
  465. .name { font-size: 32rpx; color: #222; font-weight: 500; margin-right: 20rpx; }
  466. .phone { font-size: 32rpx; color: #222; }
  467. .default-tag {
  468. margin-left: 18rpx;
  469. font-size: 24rpx;
  470. color: #FF9500;
  471. border: 1rpx solid #FF9500;
  472. border-radius: 8rpx;
  473. padding: 2rpx 12rpx;
  474. background: #FFF7E6;
  475. vertical-align: middle;
  476. }
  477. }
  478. .address-text {
  479. font-size: 28rpx;
  480. color: #888;
  481. margin: 16rpx 0 0 0;
  482. line-height: 1.5;
  483. }
  484. .dashed-line {
  485. border-bottom: 1rpx dashed #eee;
  486. margin: 24rpx 0 0 0;
  487. }
  488. .address-actions {
  489. display: flex;
  490. align-items: center;
  491. justify-content: space-around;
  492. padding: 0 0 18rpx 0;
  493. .action {
  494. display: flex;
  495. align-items: center;
  496. margin-right: 48rpx;
  497. .circle {
  498. width: 36rpx; height: 36rpx; border-radius: 50%;
  499. border: 2rpx solid #FF9500; margin-right: 8rpx;
  500. display: flex; align-items: center; justify-content: center;
  501. &.active {
  502. background: #FF9500; border: none;
  503. .check { color: #fff; font-size: 24rpx; }
  504. }
  505. }
  506. text { font-size: 26rpx; color: #bbb; }
  507. .active-text { color: #FF9500; }
  508. }
  509. }
  510. }
  511. }
  512. .bottom-bar {
  513. position: fixed;
  514. left: 0; right: 0; bottom: 0;
  515. background: #fff;
  516. padding: 24rpx 32rpx env(safe-area-inset-bottom);
  517. z-index: 10;
  518. }
  519. .main-btn {
  520. width: 100%;
  521. height: 90rpx;
  522. background: linear-gradient(90deg, #FFB74D 0%, #FF9500 100%);
  523. color: #fff;
  524. font-size: 32rpx;
  525. border-radius: 45rpx;
  526. font-weight: bold;
  527. margin: 0 auto;
  528. display: flex; align-items: center; justify-content: center;
  529. box-shadow: 0 4rpx 16rpx rgba(255, 149, 0, 0.08);
  530. }
  531. .modal-mask {
  532. position: fixed; left: 0; right: 0; top: 0; bottom: 0;
  533. background: rgba(0,0,0,0.5); z-index: 1000;
  534. }
  535. .address-modal, .region-modal {
  536. position: fixed; left: 0; right: 0; bottom: 0; z-index: 1001;
  537. background: #fff; border-radius: 32rpx 32rpx 0 0;
  538. box-shadow: 0 -4rpx 32rpx rgba(0,0,0,0.08);
  539. padding-bottom: env(safe-area-inset-bottom);
  540. animation: slideUp 0.2s;
  541. }
  542. @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
  543. .modal-header {
  544. display: flex; align-items: center; justify-content: space-between;
  545. padding: 32rpx 32rpx 0 32rpx;
  546. .close-btn { color: #bbb; font-size: 30rpx; }
  547. .modal-title { flex: 1; text-align: center; font-size: 34rpx; font-weight: bold; color: #222; }
  548. }
  549. .modal-form {
  550. padding: 0 32rpx;
  551. .form-item {
  552. border-bottom: 1rpx solid #f2f2f2;
  553. padding: 28rpx 0 10rpx 0;
  554. .label { color: #222; font-size: 28rpx; margin-bottom: 8rpx; }
  555. .input, .region-input {
  556. width: 100%; font-size: 28rpx; color: #222; background: none; border: none; outline: none;
  557. &.placeholder { color: #ccc; }
  558. }
  559. .region-input { display: flex; align-items: center; justify-content: space-between; }
  560. .arrow { color: #ccc; font-size: 28rpx; margin-left: 10rpx; }
  561. }
  562. }
  563. .region-picker {
  564. padding: 32rpx 0 0 0;
  565. .picker-view {
  566. width: 100%; height: 300rpx; display: flex; justify-content: center; align-items: center;
  567. .active { color: #222; font-weight: bold; }
  568. view { color: #ccc; font-size: 28rpx; text-align: center; }
  569. }
  570. .item {
  571. // line-height: 100upx;
  572. text-align: center;
  573. display: flex;
  574. align-items: center;
  575. justify-content: center;
  576. // height: 100upx;
  577. font-size: 30rpx;
  578. color: #222;
  579. }
  580. }
  581. </style>