Эх сурвалжийг харах

feat: 收获地址相关接口调试

liangan 2 долоо хоног өмнө
parent
commit
4bd76831a4

+ 8 - 0
src/api/common.ts

@@ -1,3 +1,4 @@
+import { qs } from '@/utils'
 import { http } from '@/utils/http'
 
 /**
@@ -7,3 +8,10 @@ import { http } from '@/utils/http'
 export function getEnum(data: any) {
   return http.get<any>('/cif/api/user/getEnum', data)
 }
+/**
+ * 获取地区
+ * @returns
+ */
+export function divisionsTreeList(data: any) {
+  return http.post<any>(`/operating/divisions/treeList?${qs(data)}`)
+}

+ 35 - 0
src/api/mine.ts

@@ -0,0 +1,35 @@
+import { extractAndRetained, qs } from '@/utils'
+import { http } from '@/utils/http'
+
+/**
+ * 新增收获地址
+ * @returns
+ */
+export function addressAdd(data: any) {
+  return http.post<any>('/mall/user/address/add', data)
+}
+
+/**
+ * 收获地址列表
+ * @returns
+ */
+export function addressList(data: any) {
+  const { extract, retained } = extractAndRetained(data, ['page', 'size'])
+  return http.post<any>(`/mall/user/address/list?${qs(extract)}`, retained)
+}
+
+/**
+ * 收获地址详情 入参id
+ * @returns
+ */
+export function addressDetail(data: any) {
+  return http.post<any>(`/mall/user/address/detail?${qs(data)}`)
+}
+
+/**
+ * 收获地址更新
+ * @returns
+ */
+export function addressUpdate(data: any) {
+  return http.post<any>(`/mall/user/address/update`, data)
+}

+ 1 - 0
src/pages.json

@@ -227,6 +227,7 @@
       "path": "pages/vipMembership/vipMembership",
       "type": "page",
       "layout": "default",
+      "needLogin": true,
       "style": {
         "navigationBarTitleText": "VIP Membership",
         "navigationBarBackgroundColor": "#FFFFFF"

+ 34 - 23
src/pages/mine/addressBook.vue

@@ -13,48 +13,59 @@
 // eslint-disable-next-line unused-imports/no-unused-imports
 import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
 import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js'
+import { addressList } from '@/api/mine'
+import { toPage } from '@/utils/page'
 
 defineOptions({
   name: 'AddressBook', // 地址簿
 })
+
+const userInfo = computed(() => {
+  return getUserInfoHook()
+})
+
 // z-paging
 const paging = ref(null)
 // 类似mixins,如果是页面滚动务必要写这一行,并传入当前ref绑定的paging,注意此处是paging,而非paging.value
 useZPaging(paging)
 // 搜索结果
 const dataList = ref([])
-function queryList(pageNo, pageSize) {
-  // 此处请求仅为演示,请替换为自己项目中的请求
-  setTimeout(() => {
-    dataList.value = [
-      { title: '123' },
-      { title: '123' },
-      { title: '123' },
-      { title: '12345' },
-    ]
-    paging.value.complete(dataList.value)
-  }, 1000)
+async function queryList(pageNo: number, pageSize: number) {
+  try {
+    const res = await addressList({
+      page: pageNo,
+      size: pageSize,
+      uid: userInfo.value.userId,
+    })
+    paging.value.complete(res.data.list)
+  }
+  catch {
+    paging.value.complete(false)
+  }
 }
-function toPage(url: string) {
-  uni.navigateTo({
-    url,
-  })
+
+// 编辑地址
+function editAddress(id: any) {
+  toPage('/pages/mine/addressBookOperate', { id })
 }
 </script>
 
 <template>
-  <z-paging ref="paging" use-page-scroll refresher-only @query="queryList">
+  <z-paging ref="paging" v-model="dataList" use-page-scroll @query="queryList">
     <view class="py-20rpx">
-      <view class="flex items-center justify-between bg-white px-22rpx py-18rpx">
+      <view v-for="item in dataList" :key="item.id" class="flex items-center justify-between bg-white px-22rpx py-18rpx" @click="editAddress(item.id)">
         <view class="flex-1">
-          <view class="mb-20rpx text-24rpx">
-            <text class="mr-20rpx">
-              Aamir Khan
-            </text>
-            <text>0642251008</text>
+          <view class="mb-20rpx flex items-center justify-between text-24rpx">
+            <view>
+              <text class="mr-20rpx">
+                {{ item.realName }}
+              </text>
+              <text>{{ item.phone }}</text>
+            </view>
+            <wd-text v-if="item.isDefault" type="primary" text="default" />
           </view>
           <view class="text-22rpx text-#3A444C">
-            55/66 The Bliss Koolpunt Vlile 16, P3QM+RW8, San Kamphaeng, San Kamphaeng District, Chiang Mai  50130
+            {{ [item.province, item.city, item.district, item.street].filter(Boolean).join(', ') }} {{ item.postCode }}
           </view>
         </view>
         <wd-icon name="arrow-right" custom-class="flex-shrink-0 ml-8rpx" color="#7D7D7D" size="24rpx" />

+ 279 - 28
src/pages/mine/addressBookOperate.vue

@@ -13,80 +13,331 @@
 // eslint-disable-next-line unused-imports/no-unused-imports
 import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
 import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js'
+import { divisionsTreeList } from '@/api/common'
+import { addressAdd, addressDetail, addressUpdate } from '@/api/mine'
+import { getPageParams, goBack } from '@/utils/page'
+import { toast } from '@/utils/toast'
 
 defineOptions({
   name: 'AddressBookOperate', // 地址簿新增&编辑
 })
+
+const userInfo = computed(() => {
+  return getUserInfoHook()
+})
 // z-paging
 const paging = ref(null)
 // 类似mixins,如果是页面滚动务必要写这一行,并传入当前ref绑定的paging,注意此处是paging,而非paging.value
 useZPaging(paging)
 
-const model = ref({})
+// 编辑模式相关
+const isEditMode = ref(false)
+const addressId = ref<any>(null)
+
+const model = ref<any>({
+  areaCodes: [],
+  realName: '',
+  phone: '',
+  detail: '',
+  postCode: '',
+  isDefault: 0,
+})
+const area = ref<any[]>([[]])
+const form = ref<any>(null)
+
+// 地址显示文本(用于编辑模式回显)
+const addressDisplayText = ref('')
+
+// 监听地址选择变化,清空预设显示文本
+watch(() => model.value.areaCodes, (newVal) => {
+  if (newVal && newVal.length > 0) {
+    addressDisplayText.value = ''
+  }
+}, { deep: true })
+
+// 城市选择
+async function columnChange({ selectedItem, resolve, finish }: any) {
+  // 模拟异步请求
+  console.log(selectedItem)
+  const res = await divisionsTreeList({ pid: selectedItem.pid, name: selectedItem.name })
+  if (res.data[0].child && res.data[0].child.length) {
+    resolve(res.data[0].child)
+  }
+  else {
+    finish()
+  }
+}
+// 格式化方法 - 动态显示所有选中项
+function displayFormat(selectedItems: any[]) {
+  // 如果是编辑模式且有预设的显示文本,优先使用预设文本
+  if (isEditMode.value && addressDisplayText.value) {
+    return addressDisplayText.value
+  }
+
+  if (!selectedItems || selectedItems.length === 0) {
+    return ''
+  }
+
+  // 如果只有一个选项,直接返回
+  if (selectedItems.length === 1) {
+    return selectedItems[0].name || selectedItems[0]
+  }
+
+  // 动态拼接所有选中项,用 / 分隔
+  return selectedItems
+    .filter(item => item && (item.name || item)) // 过滤空值
+    .map(item => item.name || item) // 提取名称
+    .join('/')
+}
+
+// 存储原始地址数据(用于编辑模式保存)
+const originalAddressData = ref<any>(null)
+
+// 获取地址详情
+async function getAddressDetail() {
+  try {
+    await uni.showLoading({
+      title: 'Loading...',
+    })
+
+    const res = await addressDetail({ id: addressId.value })
+
+    if (res.code === '200' && res.data) {
+      const data = res.data
+
+      // 保存原始地址数据
+      originalAddressData.value = {
+        province: data.province,
+        city: data.city,
+        district: data.district,
+        street: data.street,
+      }
+
+      // 构建地址显示文本
+      const addressParts = [data.province, data.city, data.district, data.street].filter(Boolean)
+      addressDisplayText.value = addressParts.join('/')
+
+      model.value = {
+        areaCodes: [], // 编辑模式下先清空,用户点击时重新选择
+        realName: data.realName || '',
+        phone: data.phone || '',
+        detail: data.detail || '',
+        postCode: data.postCode || '',
+        isDefault: data.isDefault || false,
+      }
+
+      console.log('Address detail loaded:', data)
+      console.log('Display text:', addressDisplayText.value)
+    }
+    else {
+      toast.error(res.message || 'Failed to load address details')
+    }
+  }
+  catch (error: any) {
+    console.error('Get address detail error:', error)
+    toast.error(error.message || 'Failed to load address details')
+  }
+  finally {
+    uni.hideLoading()
+  }
+}
+
+// 保存地址
+async function save() {
+  try {
+    // 自定义校验
+    if (!model.value.realName?.trim()) {
+      toast.info('Please enter full name')
+      return
+    }
+
+    if (!model.value.phone?.trim()) {
+      toast.info('Please enter phone number')
+      return
+    }
+
+    // 在新增模式下必须选择地址,编辑模式下可以使用原有地址
+    if (!isEditMode.value && (!model.value.areaCodes || model.value.areaCodes.length === 0)) {
+      toast.info('Please select province/district')
+      return
+    }
+
+    if (!model.value.detail?.trim()) {
+      toast.info('Please enter detailed address')
+      return
+    }
+
+    if (!model.value.postCode?.trim()) {
+      toast.info('Please enter postcode')
+      return
+    }
+
+    // 显示加载提示
+    await uni.showLoading({
+      title: 'Saving...',
+    })
+
+    // 处理地址数据
+    let addressData: any = {}
+
+    if (model.value.areaCodes && model.value.areaCodes.length > 0) {
+      // 用户重新选择了地址
+      addressData = {
+        province: model.value.areaCodes[0],
+        city: model.value.areaCodes[1],
+        district: model.value.areaCodes[2],
+        street: model.value.areaCodes[3],
+      }
+    }
+    else if (isEditMode.value && originalAddressData.value) {
+      // 编辑模式下用户没有重新选择地址,使用原始数据
+      addressData = originalAddressData.value
+    }
+    else {
+      // 新增模式下必须选择地址
+      toast.info('Please select province/district')
+      return
+    }
+
+    const params = {
+      ...model.value,
+      realName: model.value.realName.trim(),
+      phone: model.value.phone.trim(),
+      detail: model.value.detail.trim(),
+      postCode: model.value.postCode.trim(),
+      ...addressData,
+      uid: userInfo.value.userId,
+    }
+
+    // 如果是编辑模式,添加id参数并调用更新接口
+    if (isEditMode.value && addressId.value) {
+      params.id = addressId.value
+    }
+
+    const res = isEditMode.value
+      ? await addressUpdate(params)
+      : await addressAdd(params)
+
+    uni.hideLoading()
+
+    if (res.code === '200') {
+      toast.success(isEditMode.value ? 'Address updated successfully' : 'Address saved successfully')
+
+      // 延迟返回上一页
+      setTimeout(() => {
+        goBack()
+      }, 1500)
+    }
+    else {
+      toast.error(res.message || 'Save failed, please try again')
+    }
+  }
+  catch (error: any) {
+    uni.hideLoading()
+    console.error('Save address error:', error)
+    toast.error(error.message || 'Save failed, please try again')
+  }
+}
+
+onLoad(async (options: any) => {
+  try {
+    // 获取地区数据
+    const res = await divisionsTreeList({})
+    console.log(res)
+    area.value = [res.data]
+
+    // 检查是否为编辑模式
+    const params = getPageParams(options)
+    if (params.id) {
+      isEditMode.value = true
+      addressId.value = params.id
+      // 设置页面标题
+      uni.setNavigationBarTitle({
+        title: 'Edit Address',
+      })
+      // 获取地址详情
+      await getAddressDetail()
+    }
+    else {
+      // 新增模式
+      uni.setNavigationBarTitle({
+        title: 'Add Address',
+      })
+    }
+  }
+  catch (error: any) {
+    console.error('Page load error:', error)
+    toast.error(error.message || 'Page load failed')
+  }
+})
 </script>
 
 <template>
   <z-paging ref="paging" use-page-scroll>
     <view class="py-20rpx">
       <wd-form ref="form" :model="model">
-        <wd-cell-group border>
+        <wd-cell-group>
           <wd-input
-            v-model="model.value1"
+            v-model="model.realName"
             label="Full Name"
             label-width="240rpx"
             custom-label-class="text-28rpx"
-            prop="value1"
             clearable
             placeholder="Full Name"
-            :rules="[{ required: true, message: 'Full Name is required' }]"
+            required
           />
           <wd-input
-            v-model="model.value1"
+            v-model="model.phone"
             label="Phone Number"
             label-width="240rpx"
             custom-label-class="text-28rpx"
-            prop="value1"
             clearable
             placeholder="+88"
-            :rules="[{ required: true, message: 'Phone Number is required' }]"
-          />
-          <wd-input
-            v-model="model.value1"
-            label="Privince/District"
-            label-width="240rpx"
-            custom-label-class="text-28rpx"
-            prop="value1"
-            clearable
-            placeholder="Please choose"
-            :rules="[{ required: true, message: 'Privince/District is required' }]"
+            required
           />
+          <wd-cell title="Privince/District" required vertical>
+            <wd-col-picker
+              v-model="model.areaCodes"
+              :columns="area"
+              clearable
+              placeholder="Please choose"
+              value-key="name"
+              label-key="name"
+              :column-change="columnChange"
+              required
+              :root-portal="true"
+              :z-index="9999"
+              :display-format="displayFormat"
+            />
+          </wd-cell>
+
           <wd-cell title="Floor/Unit No./Street" vertical required>
             <wd-textarea
-              v-model="model.value1"
-
-              prop="value1"
-              clearable auto-height
+              v-model="model.detail"
+              clearable
+              auto-height
               placeholder="Detailed address"
-              :rules="[{ required: true, message: 'Floor/Unit No./Street is required' }]"
             />
           </wd-cell>
           <wd-input
-            v-model="model.value1"
+            v-model="model.postCode"
             label="Postcode"
             label-width="240rpx"
             custom-label-class="text-28rpx"
-            prop="value1"
             clearable
             placeholder="Your postcode"
-            :rules="[{ required: true, message: 'Postcode is required' }]"
+            required
           />
+          <wd-cell title="Default">
+            <wd-switch v-model="model.isDefault" :active-value="1" :inactive-value="0" size="42rpx" />
+          </wd-cell>
         </wd-cell-group>
       </wd-form>
     </view>
     <template #bottom>
       <view class="bg-white/60 px-28rpx py-30rpx backdrop-blur-20">
-        <wd-button plain block>
-          Save
+        <wd-button plain block @click="save">
+          {{ isEditMode ? 'Update' : 'Save' }}
         </wd-button>
       </view>
     </template>

+ 18 - 10
src/pages/vipMembership/vipMembership.vue

@@ -1,6 +1,7 @@
 <route lang="json5" type="page">
 {
   layout: 'default',
+  needLogin: true,
   style: {
     navigationBarTitleText: 'VIP Membership',
     navigationBarBackgroundColor: '#FFFFFF',
@@ -9,9 +10,16 @@
 </route>
 
 <script lang="ts" setup>
+import { formatNumber } from '@/utils'
+
 defineOptions({
   name: 'VipMembership', // 会员中心
 })
+
+const userInfo = computed(() => {
+  return getUserInfoHook()
+})
+
 interface TableData {
   vipLevel: string
   invitedNo: string
@@ -121,19 +129,19 @@ const dataList = ref<TableData[]>([
       style="background-image: url('/static/images/vip-info-bg.png');"
     >
       <view>
-        <wd-img width="100rpx" height="100rpx" custom-class="mb-18rpx" round src="/static/images/default-avatar.png" />
+        <wd-img width="100rpx" height="100rpx" custom-class="mb-18rpx" round :src="userInfo.headPic" />
         <view class="text-32rpx text-white font-bold">
-          Aamir Khan
+          {{ userInfo.name }}
         </view>
-        <wd-progress :duration="0" custom-class="w-85%!" color="#E7BEA6" :percentage="60" hide-text />
+        <wd-progress :duration="0" custom-class="w-85%!" color="#E7BEA6" :percentage="(userInfo.invitedNo / userInfo.nextInvitedNo) * 100" hide-text />
         <view class="text-22rpx text-#714428 font-bold">
-          <text>We still need to invite</text>
+          <text>We still need to invite </text>
           <text class="text-white">
-            10 friends
+            {{ formatNumber((userInfo.nextInvitedNo - userInfo.invitedNo), 0) }} friends
           </text>
           <text>. Can upgrade to</text>
           <text class="text-white">
-            V3
+            V{{ userInfo.level }}
           </text>
         </view>
       </view>
@@ -146,16 +154,16 @@ const dataList = ref<TableData[]>([
             Invited Friends
           </view>
           <view class="text-26rpx font-bold">
-            12,566
+            {{ formatNumber(userInfo.invitedNo, 0) }}
           </view>
         </view>
-        <wd-divider dashed custom-class="h-40rpx!" color="#A4A4A4" vertical />
+        <wd-divider custom-class="h-40rpx!" color="#A4A4A4" vertical dashed />
         <view class="flex-[33.33%]">
           <view class="text-22rpx text-#5B5B5B">
             Team Members
           </view>
           <view class="text-26rpx font-bold">
-            12,566
+            {{ formatNumber(userInfo.teamNo, 0) }}
           </view>
         </view>
         <wd-divider dashed custom-class="h-40rpx!" color="#A4A4A4" vertical />
@@ -164,7 +172,7 @@ const dataList = ref<TableData[]>([
             L7D Earnings
           </view>
           <view class="text-26rpx font-bold">
-            12,566
+            {{ formatNumber(userInfo.l7DEarnings) }}
           </view>
         </view>
       </view>

+ 26 - 5
src/utils/index.ts

@@ -229,10 +229,31 @@ export function parseQs(queryString) {
     }, {})
 }
 
-// 数字格式化,加逗号 保留两位小数 增加判空处理
-export function formatNumber(num) {
-  if (num === undefined || num === null) {
-    return '0.00'
+/**
+ * 数字格式化,加逗号分隔符,支持自定义小数位数
+ * @param num 要格式化的数字,支持 number 或 string 类型
+ * @param decimals 小数位数,默认为 2,传入 0 表示不要小数
+ * @returns 格式化后的字符串
+ */
+export function formatNumber(num: number | string, decimals: number = 2): string {
+  // 处理空值
+  if (num === undefined || num === null || num === '') {
+    return decimals === 0 ? '0' : `0.${'0'.repeat(decimals)}`
   }
-  return num.toFixed(2).replace(/\d{1,3}(?=(\d{3})+(\.\d*)?$)/g, '$&,')
+
+  // 转换为数字
+  const numValue = typeof num === 'string' ? Number.parseFloat(num) : num
+
+  // 检查是否为有效数字
+  if (Number.isNaN(numValue)) {
+    return decimals === 0 ? '0' : `0.${'0'.repeat(decimals)}`
+  }
+
+  // 格式化数字
+  const formattedNum = decimals === 0
+    ? Math.round(numValue).toString()
+    : numValue.toFixed(decimals)
+
+  // 添加千分位分隔符
+  return formattedNum.replace(/\d{1,3}(?=(\d{3})+(\.\d*)?$)/g, '$&,')
 }