Forráskód Böngészése

fix: 地址管理功能优化

7746 1 hete
szülő
commit
54454e9041

+ 48 - 3
src/api/user.js

@@ -41,6 +41,13 @@ export const userLeaveMessage = params => request({
   params: params
 })
 
+// 获取提货时间列表
+export const getPickTimeList = params => request({
+  url: '/common/order/pick/time/list',
+  method: 'post',
+  params: params
+})
+
 // 获取地址列表
 export const getAddressList = params => request({
   url: '/user/address/list',
@@ -48,9 +55,47 @@ export const getAddressList = params => request({
   params: params
 })
 
-// 获取提货时间列表
-export const getPickTimeList = params => request({
-  url: '/common/order/pick/time/list',
+// 根据中文地址反查经纬度省市区街道
+export const queryAddress = params => request({
+  url: '/lbs/amap/region/query2',
+  method: 'post',
+  params: params
+})
+
+// 新增地址
+export const addAddress = params => request({
+  url: '/user/address/save',
+  method: 'post',
+  data: params,
+  json: true
+})
+
+// 修改地址
+export const updateAddress = params => request({
+  url: '/user/address/update',
+  method: 'post',
+  params: params,
+  data: params,
+  json: true
+})
+
+// 删除地址
+export const delAddress = params => request({
+  url: '/user/address/del',
+  method: 'post',
+  params: params
+})
+
+// 地址详情
+export const getAddressDetail = params => request({
+  url: '/user/address/detail',
+  method: 'get',
+  params: params
+})
+
+// 查询省市区街道
+export const queryRegion = params => request({
+  url: '/common/region',
   method: 'post',
   params: params
 })

+ 206 - 0
src/pages/home/components/AddressForm.vue

@@ -0,0 +1,206 @@
+<template>
+  <div class="edit-address">
+    <a-page-header
+      style="padding: 0;margin-bottom: 10px;"
+      title="返回"
+      @back="emits('prev-step')"
+    />
+    <a-form
+      name="mainFormRef"
+      :model="mainForm"
+      :label-col="{ style: {width: '100px'}}"
+      @finish="onFinish"
+    >
+      <a-form-item
+        label="联系人"
+        name="name"
+        :rules="[{ required: true, message: '不能为空' }]"
+      >
+        <a-input v-model:value="mainForm.name" placeholder="请输入" allowClear />
+      </a-form-item>
+      <a-form-item
+        label="手机号码"
+        name="phone"
+        :rules="[{ required: true, validator: checkPhone }]"
+      >
+        <a-input v-model:value="mainForm.phone" placeholder="请输入" allowClear />
+      </a-form-item>
+      <a-form-item
+        label="所在地区"
+        name="area"
+        :rules="[{ required: true, message: '不能为空' }]"
+      >
+        <a-cascader
+          v-model:value="mainForm.area"
+          :options="mainForm.options"
+          :load-data="loadData"
+          placeholder="请选择"
+          change-on-select
+          :fieldNames="{
+            label: 'name',
+            value: 'name',
+            children: 'children'
+          }"
+          @focus="loadFirstLevelRegin"
+        />
+      </a-form-item>
+      <a-form-item
+        label="详细地址"
+        name="address"
+        :rules="[{ required: true, message: '不能为空' }]"
+      >
+        <a-textarea v-model:value="mainForm.address" placeholder="请输入" allowClear :auto-size="{minRows: 3, maxRows: 5}" />
+      </a-form-item>
+      <a-form-item
+        label="门牌号"
+        name="houseNo"
+      >
+        <a-input v-model:value="mainForm.houseNo" placeholder="请输入" allowClear />
+      </a-form-item>
+      <a-form-item
+        label="设为默认"
+        name="defaultAddr"
+      >
+        <a-switch v-model:checked="mainForm.defaultAddr" />
+      </a-form-item>
+      <a-form-item label=" " :colon="false">
+        <a-button :disabled="mainForm.disabled" type="primary" html-type="submit" block>提 交</a-button>
+      </a-form-item>
+    </a-form>
+  </div>
+</template>
+
+<script setup lang="js">
+import { reactive, onMounted } from 'vue';
+import { message } from 'ant-design-vue';
+import { addAddress, updateAddress, getAddressDetail, queryRegion } from '@/api/user';
+import { validPhone } from '@/utils/validate';
+import { useUserStore } from '@/store/user';
+
+const props = defineProps({
+  addressId: {
+    type: String,
+    default: ''
+  },
+  type: {
+    type: String,
+    default: 'add'
+  }
+})
+const emits = defineEmits(['prev-step', 'add', 'update']);
+
+const userStore = useUserStore();
+
+const mainForm = reactive({
+  name: undefined,
+  phone: undefined,
+  area: undefined,
+  address: undefined,
+  houseNo: undefined,
+  defaultAddr: false,
+  disabled: false,
+  options: []
+})
+
+const onFinish = () => {
+  mainForm.disabled = true
+  const [
+    province,
+    city,
+    area,
+    street
+  ] = mainForm.area;
+  const params = {
+    province,
+    city,
+    area,
+    street,
+    name: mainForm.name,
+    phone: mainForm.phone,
+    address: mainForm.address,
+    houseNo: mainForm.houseNo,
+    defaultAddr: mainForm.defaultAddr,
+    userId: userStore.userInfo?.userId || ''
+  }
+  if (props.type !== 'add') {
+    params.userAddressId = props.addressId
+    updateAddress(params).then(() => {
+      message.success('修改成功')
+      emits('add')
+      emits('prev-step')
+    }).finally(() => {
+      mainForm.disabled = false
+    })
+  } else {
+    addAddress(params).then(() => {
+      message.success('提交成功')
+      emits('update')
+      emits('prev-step')
+    }).finally(() => {
+      mainForm.disabled = false
+    })
+  }
+}
+
+const fetchDetail = async () => {
+  const res = await getAddressDetail({
+    userAddressId: props.addressId
+  })
+  mainForm.name = res.data?.name
+  mainForm.phone = res.data?.phone
+  mainForm.address = res.data?.address
+  mainForm.houseNo = res.data?.houseNo
+  mainForm.defaultAddr = res.data?.defaultAddr
+  mainForm.area = [res.data?.province, res.data?.city, res.data?.area, res.data?.street]
+}
+
+const fetchReginData = (parentId = '') => {
+  return new Promise((resolve, reject) => {
+    queryRegion({parentId}).then(res => resolve(res)).catch(() => {
+      reject()
+    })
+  })
+}
+
+const loadFirstLevelRegin = async() => {
+  if (mainForm.options.length === 0) {
+    const res = await fetchReginData('');
+    mainForm.options = (res.data || []).map(item => ({
+      ...item,
+      isLeaf: false
+    }))
+  }
+}
+
+const loadData = async (selectedOptions) => {
+  const targetOption = selectedOptions[selectedOptions.length - 1];
+  if (targetOption.level >= 4) {
+    targetOption.loading = false;
+    return;
+  }
+  targetOption.loading = true;
+  const res = await fetchReginData(targetOption.id).finally(() => {
+    targetOption.loading = false;
+  })
+  if (res) {
+    targetOption.loading = false;
+    targetOption.children = (res.data || []).map(item => ({
+      ...item,
+      isLeaf: targetOption.level >= 3
+    }))
+    mainForm.options = [...mainForm.options];
+  }
+}
+
+const checkPhone= (rule, value) => {
+  if (value === '') return Promise.reject('不能为空')
+  if (validPhone(value) === false) return Promise.reject('手机号码格式有误')
+  return Promise.resolve()
+}
+
+onMounted(() => {
+  if (props.type !== 'add' && props.addressId) {
+    fetchDetail()
+  }
+})
+</script>

+ 102 - 0
src/pages/home/components/AddressList.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="address-list">
+    <template v-if="mainData.list.length > 0">
+      <a-card
+        v-for="item in mainData.list"
+        :key="item.userAddressId"
+        size="small"
+        style="margin-bottom: 10px;"
+      >
+        <template #title>{{item.province}} {{item.city}} {{item.area}} {{item.street}}</template>
+        <template #extra>
+          <a-button v-if="showSelect" size="small" type="link" @click="handleSelect(item)">选择</a-button>
+          <a-button v-if="showEdit" size="small" type="link" @click="handleEdit(item)">编辑</a-button>
+          <a-popconfirm
+            title="是否确定删除?"
+            ok-text="确定"
+            cancel-text="取消"
+            @confirm="handleDelete(item)"
+          >
+            <a-button size="small" type="link" danger>删除</a-button>
+          </a-popconfirm>
+        </template>
+        <div>{{item.address}}{{item.houseNo}}</div>
+        <div>{{item.name}} {{item.phone}}</div>
+      </a-card>
+      <div v-if="mainData.total > mainData.list.length" style="text-align: center;">
+        <a-button type="link" :disabled="mainData.disabled" @click="handleLoadMore">点击查看更多</a-button>
+      </div>
+    </template>
+    <a-empty v-else />
+  </div>
+</template>
+
+<script setup lang="js">
+import { message } from 'ant-design-vue';
+import { reactive, onMounted } from 'vue';
+import { getAddressList, delAddress } from '@/api/user';
+import { useUserStore } from '@/store/user';
+
+const props = defineProps({
+  showEdit: {
+    type: Boolean,
+    default: true
+  },
+  showSelect: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emits = defineEmits(['select', 'edit', 'delete'])
+
+const userStore = useUserStore();
+
+const mainData = reactive({
+  list: [],
+  pageNum: 1,
+  total: 0,
+  disabled: false
+})
+
+const fetchAddressList = async () => {
+  getAddressList({
+    pageNum: mainData.pageNum,
+    pageSize: 20,
+    userId: userStore.userInfo?.userId || ''
+  }).then(res => {
+    mainData.list = mainData.list.concat(res.data?.records || []);
+    mainData.total = res.data?.total;
+  }).finally(() => {
+    mainData.disabled = false
+  })
+}
+
+const handleLoadMore = () => {
+  mainData.disabled = true
+  mainData.pageNum += 1
+  fetchAddressList()
+}
+
+const handleSelect = row => emits('select', row)
+
+const handleEdit = row => emits('edit', row)
+
+const handleDelete = row => {
+  delAddress({
+    userAddressId: row.userAddressId
+  }).then(() => {
+    mainData.list = []
+    mainData.pageNum = 1
+    mainData.total = 0
+    fetchAddressList()
+    message.success('删除成功')
+    emits('delete')
+  })
+}
+
+onMounted(() => {
+  fetchAddressList()
+})
+
+</script>

+ 112 - 0
src/pages/home/components/AddressModal.vue

@@ -0,0 +1,112 @@
+<template>
+  <a-drawer
+    :open="open"
+    title="地址管理"
+    width="620px"
+    placement="right"
+    @close="emits('close')"
+  >
+    <div class="drawer-box">
+      <div class="drawer-content">
+        <AddressList
+          v-if="mainData.stepNum == 1"
+          @select="selectFn"
+          @edit="editFn"
+          @delete="handleFn"
+        />
+        <AddressForm
+          v-if="mainData.stepNum == 2"
+          :type="mainData.handleType"
+          :address-id="mainData.addressId"
+          @add="handleFn"
+          @update="handleFn"
+          @prev-step="prevStepFn"
+        />
+      </div>
+      <template v-if="mainData.stepNum == 1">
+        <a-divider style="margin: 10px 0;" />
+        <a-button type="primary" block @click="handleAddNew">新增地址</a-button>
+      </template>
+    </div>
+  </a-drawer>
+</template>
+
+<script setup lang="js">
+import AddressList from './AddressList.vue';
+import AddressForm from './AddressForm.vue';
+import { reactive, nextTick, watch } from 'vue';
+
+const props = defineProps({
+  open: {
+    type: Boolean,
+    default: false
+  },
+  showEdit: {
+    type: Boolean,
+    default: true
+  },
+  showSelect: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emits = defineEmits(['close', 'select', 'change']);
+
+const HandleTypeEnum = {
+  ADD: 'add',
+  UPDATE: 'update'
+}
+const mainData = reactive({
+  stepNum: 0,
+  addressId: '',
+  handleType: HandleTypeEnum.ADD
+})
+
+const selectFn = row => emits('select', row);
+
+const editFn = row => {
+  mainData.stepNum = 2
+  mainData.addressId = row.userAddressId
+  mainData.handleType = HandleTypeEnum.UPDATE
+}
+
+const handleAddNew = () => {
+  mainData.stepNum = 2
+  mainData.addressId = ''
+  mainData.handleType = HandleTypeEnum.ADD
+}
+
+const prevStepFn = () => {
+  mainData.addressId = ''
+  mainData.stepNum -= 1
+}
+
+const handleFn = () => {
+  emits('change')
+}
+
+watch(() => props.open, newVal => {
+  if (newVal) {
+    mainData.stepNum = 0
+    mainData.addressId = ''
+    nextTick(() => {
+      mainData.stepNum = 1
+    })
+  }
+})
+
+</script>
+
+<style lang="less" scoped>
+.drawer-box {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  .drawer-content {
+    flex: 1;
+    min-height: 1px;
+    overflow-y: auto;
+  }
+}
+</style>

+ 10 - 11
src/pages/home/components/OrderDetail.vue

@@ -20,11 +20,11 @@
       <a-alert v-if="mainData.detail.orderStatus == 'CLOSE'" message="订单已关闭" type="info" />
       <a-alert v-if="mainData.detail.orderStatus == 'TIMEOUT'" message="订单超时未支付" type="info" />
     </div>
-    <MainCard v-if="mainData.detail.takeGoodsType == TakeTypeEnum.DISPATCH" title="收货人信息" style="margin-bottom: 15px;">
+    <a-card size="small" v-if="mainData.detail.takeGoodsType == TakeTypeEnum.DISPATCH" title="收货人信息" style="margin-bottom: 15px;">
       <div class="row-text__content">{{ mainData.detail.receUserName }} {{ mainData.detail.recePhone }}</div>
       <div class="row-text__content">{{ mainData.detail.province }}{{ mainData.detail.city }}{{ mainData.detail.area }}{{ mainData.detail.street }}{{ mainData.detail.receAddress}}{{ mainData.detail.houseNo ? mainData.detail.houseNo : '' }}</div>
-    </MainCard>
-    <MainCard title="商品信息" style="margin-bottom: 15px;">
+    </a-card>
+    <a-card size="small" title="商品信息" style="margin-bottom: 15px;">
       <div class="goods-box">
         <div v-for="(item, index) in mainData.detail.orderDetails" :key="index" class="goods-item">
           <img class="goods-image" :src="item.imgUrl" />
@@ -43,8 +43,8 @@
           </div>
         </div>
       </div>
-    </MainCard>
-    <MainCard style="margin-bottom: 15px;">
+    </a-card>
+    <a-card size="small" style="margin-bottom: 15px;">
       <div class="row-item__box">
         <div class="row-item__content">
           <div class="row-item__label">{{ mainData.detail.takeGoodsType == TakeTypeEnum.DISPATCH ? '配送时间' : '提货时间' }}</div>
@@ -55,8 +55,8 @@
           <div class="row-item__value">{{ mainData.detail.buyerMsg }}</div>
         </div>
       </div>
-    </MainCard>
-    <MainCard style="margin-bottom: 15px;">
+    </a-card>
+    <a-card size="small" style="margin-bottom: 15px;">
       <div class="row-item__box">
         <div class="row-item__content">
           <div class="row-item__label">商品金额</div>
@@ -71,8 +71,8 @@
           <div class="row-item__value">¥{{ formatPriceText(mainData.detail.payAmount) }}</div>
         </div>
       </div>
-    </MainCard>
-    <MainCard title="订单信息">
+    </a-card>
+    <a-card size="small" title="订单信息">
       <div class="row-item__box">
         <div class="row-item__content">
           <div class="row-item__label">订单编码</div>
@@ -99,12 +99,11 @@
           <div class="row-item__value">{{ mainData.detail.overTime }}</div>
         </div>
       </div>
-    </MainCard>
+    </a-card>
   </div>
 </template>
 
 <script setup lang="js">
-import MainCard from '@/components/card/index.vue';
 import { reactive, onMounted, computed } from 'vue';
 import { getOrderDetail, getRefundDetail } from '@/api/order';
 import { TakeTypeEnum } from '@/utils/enum';

+ 66 - 7
src/pages/home/components/SubmitCart.vue

@@ -50,10 +50,25 @@
         >
           <a-select
             v-model:value="mainData.userAddressId"
+            :open="mainData.openAddressPanel"
+            @dropdownVisibleChange="open => mainData.openAddressPanel = open"
             style="width: 100%"
             placeholder="请选择收货地址"
             :options="mainData.addressList"
-          />
+          >
+            <template #dropdownRender="{ menuNode: menu }">
+              <v-nodes :vnodes="menu" />
+              <a-divider style="margin: 4px 0" />
+              <div style="padding: 4px 10px; text-align: center;">
+                <a-button type="link" @click="toAddAddress">
+                  <template #icon>
+                    <PlusOutlined />
+                  </template>
+                  <span>新增地址</span>
+                </a-button>
+              </div>
+            </template>
+          </a-select>
         </a-form-item>
         <a-form-item
           label="出货仓库"
@@ -155,14 +170,23 @@
         </a-flex>
       </a-flex>
     </div>
+    <AddressModal
+      :show-select="true"
+      :show-edit="true"
+      :open="mainData.addressOpen"
+      @change="changeAddressFn"
+      @close="mainData.addressOpen = false"
+      @select="selectAddressFn"
+    />
   </div>
 </template>
 
 <script setup lang="js">
 import dayjs from 'dayjs';
+import AddressModal from './AddressModal.vue';
 import { message } from 'ant-design-vue';
 import { MinusOutlined, PlusOutlined } from '@ant-design/icons-vue';
-import { ref, reactive, onMounted, computed } from 'vue';
+import { ref, reactive, nextTick, onMounted, computed, defineComponent } from 'vue';
 import { ackOrder, orderBuy } from '@/api/order';
 import { getAddressList, getPickTimeList } from '@/api/user';
 import { useUserStore } from '@/store/user';
@@ -189,6 +213,18 @@ const props = defineProps({
   }
 })
 
+const VNodes = defineComponent({
+  props: {
+    vnodes: {
+      type: Object,
+      required: true,
+    },
+  },
+  render() {
+    return this.vnodes
+  }
+})
+
 const formRef = ref(null)
 const mainData = reactive({
   goodsList: [],
@@ -206,7 +242,9 @@ const mainData = reactive({
   freight: '',
   isGiftGoods: false,
   isFullPieceGoods: false,
-  disabled: false
+  disabled: false,
+  addressOpen: false,
+  openAddressPanel: false
 })
 
 const storageName = computed(() => {
@@ -218,10 +256,10 @@ const isCreditPay = computed(() => {
   return userDetail.value.isCreditEnabled && (userDetail.value.availableCredit - payTotalAmount > 0)
 })
 
-const fetchAddressList = async() => {
+const fetchAddressList = async(refresh = false) => {
   const res = await getAddressList({
     pageNum: 1,
-    pageSize: 100,
+    pageSize: -1,
     userId: props.userId
   })
   mainData.addressList = (res.data?.records || []).map(item => {          
@@ -231,7 +269,14 @@ const fetchAddressList = async() => {
       ...item
     }
   })
-  mainData.userAddressId = mainData.addressList.find(item => item.defaultAddr)?.userAddressId || undefined;
+  if (!refresh) {
+    mainData.userAddressId = mainData.addressList.find(item => item.defaultAddr)?.userAddressId || undefined;
+  } else {
+    const target = mainData.addressList.find(item => item.userAddressId === mainData.userAddressId);
+    if (!target) {
+      mainData.userAddressId = undefined;
+    }
+  }
 }
 
 const fetchPickTimeList = async () => {
@@ -315,7 +360,6 @@ const setDispatchTimeRange = () => {
       ]
     })
   }
-  
 }
 
 const getSendTime = () => {
@@ -379,6 +423,21 @@ const formatPriceText = price => {
   return price.toFixed(2)
 }
 
+const toAddAddress = () => {
+  mainData.openAddressPanel = false
+  mainData.addressOpen = true
+}
+
+const changeAddressFn = () => {
+  fetchAddressList(true)
+}
+
+const selectAddressFn = row => {
+  fetchAddressList(true)
+  mainData.userAddressId = row.userAddressId
+  mainData.addressOpen = false
+}
+
 const checkAddress = (rule, value) => {
   if (mainData.takeGoodsType != TakeTypeEnum.DISPATCH) return Promise.resolve()
   if (!value) return Promise.reject('收获地址不能为空')

+ 1 - 1
src/pages/home/components/Toolbar.vue

@@ -4,7 +4,7 @@
       <div class="toolbar-item" @click="viewOrder">我的订单</div>
       <div class="toolbar-item">分支</div>
       <div class="toolbar-item" title="点击查看培训信息" @click="handleTrain">培训</div>
-      <div class="toolbar-item" @click="handleContact">联系我们</div>
+      <div class="toolbar-item" @click="handleContact">联系我们 </div>
       <a-dropdown placement="bottom">
         <div class="toolbar-item">语言切换</div>
         <template #overlay>