Browse Source

feat(physical): 添加订单收货地址管理和省市区功能

- 新增订单收货地址API接口实现增删改查功能
- 新增省市区API接口支持地区数据管理
- 在订单页面添加修改收货地址弹窗功能
- 实现省市区三级联动选择组件
- 添加地址表单验证和提交逻辑
- 集成地图定位和区域查询功能
- 优化订单页面UI布局和交互体验
fugui001 1 week ago
parent
commit
97f6e0de00

+ 73 - 0
src/api/system/physical/orderAddress/index.ts

@@ -0,0 +1,73 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { OrderAddressVO, OrderAddressForm, OrderAddressQuery } from '@/api/system/physical/orderAddress/types';
+
+/**
+ * 查询用户订单收货地址列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listOrderAddress = (query?: OrderAddressQuery): AxiosPromise<OrderAddressVO[]> => {
+  return request({
+    url: '/physical/orderAddress/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询用户订单收货地址详细
+ * @param id
+ */
+export const getOrderAddress = (id: string | number): AxiosPromise<OrderAddressVO> => {
+  return request({
+    url: '/physical/orderAddress/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增用户订单收货地址
+ * @param data
+ */
+export const addOrderAddress = (data: OrderAddressForm) => {
+  return request({
+    url: '/physical/orderAddress',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改用户订单收货地址
+ * @param data
+ */
+export const updateOrderAddress = (data: OrderAddressForm) => {
+  return request({
+    url: '/physical/orderAddress',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除用户订单收货地址
+ * @param id
+ */
+export const delOrderAddress = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/physical/orderAddress/' + id,
+    method: 'delete'
+  });
+};
+/**
+ * 查询用户订单收货地址详细
+ * @param id
+ */
+export const selectPhysicalOrderAddress = (orderSn: string | number): AxiosPromise<OrderAddressVO> => {
+  return request({
+    url: '/physical/orderAddress/selectPhysicalOrderAddress/' + orderSn,
+    method: 'get'
+  });
+};

+ 276 - 0
src/api/system/physical/orderAddress/types.ts

@@ -0,0 +1,276 @@
+export interface OrderAddressVO {
+  /**
+   * 主键ID
+   */
+  id: string | number;
+
+  /**
+   * 用户账号ID,外键关联 user.id
+   */
+  userId: string | number;
+
+  /**
+   * 收货人姓名
+   */
+  receiverName: string;
+
+  /**
+   * 手机号(必填)
+   */
+  mobilePhone: string;
+
+  /**
+   * 省份编码(如:440000)
+   */
+  provinceCode: string;
+
+  /**
+   * 省份名称(冗余字段,避免查联)
+   */
+  provinceName: string;
+
+  /**
+   * 城市编码(如:440100)
+   */
+  cityCode: string;
+
+  /**
+   * 城市名称(冗余字段)
+   */
+  cityName: string;
+
+  /**
+   * 区/县编码(如:440106)
+   */
+  districtCode: string;
+
+  /**
+   * 区/县名称(冗余字段)
+   */
+  districtName: string;
+
+  /**
+   * 详细地址(小区、门牌号等)
+   */
+  detailAddress: string;
+
+  /**
+   * 是否默认地址:0=否,1=是(最多一个)
+   */
+  isDefault: number;
+
+  /**
+   * 状态:正常、已删除 normal, deleted
+   */
+  status: string;
+
+  /**
+   * 创建人
+   */
+  createdBy: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt: string;
+
+  /**
+   * 地理位置编码
+   */
+  locationCode: string;
+}
+
+export interface OrderAddressForm extends BaseEntity {
+  /**
+   * 主键ID
+   */
+  id?: string | number;
+
+  /**
+   * 用户账号ID,外键关联 user.id
+   */
+  userId?: string | number;
+
+  /**
+   * 收货人姓名
+   */
+  receiverName?: string;
+
+  /**
+   * 手机号(必填)
+   */
+  mobilePhone?: string;
+
+  /**
+   * 省份编码(如:440000)
+   */
+  provinceCode?: string;
+
+  /**
+   * 省份名称(冗余字段,避免查联)
+   */
+  provinceName?: string;
+
+  /**
+   * 城市编码(如:440100)
+   */
+  cityCode?: string;
+
+  /**
+   * 城市名称(冗余字段)
+   */
+  cityName?: string;
+
+  /**
+   * 区/县编码(如:440106)
+   */
+  districtCode?: string;
+
+  /**
+   * 区/县名称(冗余字段)
+   */
+  districtName?: string;
+
+  /**
+   * 详细地址(小区、门牌号等)
+   */
+  detailAddress?: string;
+
+  /**
+   * 是否默认地址:0=否,1=是(最多一个)
+   */
+  isDefault?: number;
+
+  /**
+   * 状态:正常、已删除 normal, deleted
+   */
+  status?: string;
+
+  /**
+   * 创建人
+   */
+  createdBy?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy?: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt?: string;
+
+  /**
+   * 地理位置编码
+   */
+  locationCode?: string;
+  orderSn?: string;
+}
+
+export interface OrderAddressQuery extends PageQuery {
+  /**
+   * 用户账号ID,外键关联 user.id
+   */
+  userId?: string | number;
+
+  /**
+   * 收货人姓名
+   */
+  receiverName?: string;
+
+  /**
+   * 手机号(必填)
+   */
+  mobilePhone?: string;
+
+  /**
+   * 省份编码(如:440000)
+   */
+  provinceCode?: string;
+
+  /**
+   * 省份名称(冗余字段,避免查联)
+   */
+  provinceName?: string;
+
+  /**
+   * 城市编码(如:440100)
+   */
+  cityCode?: string;
+
+  /**
+   * 城市名称(冗余字段)
+   */
+  cityName?: string;
+
+  /**
+   * 区/县编码(如:440106)
+   */
+  districtCode?: string;
+
+  /**
+   * 区/县名称(冗余字段)
+   */
+  districtName?: string;
+
+  /**
+   * 详细地址(小区、门牌号等)
+   */
+  detailAddress?: string;
+
+  /**
+   * 是否默认地址:0=否,1=是(最多一个)
+   */
+  isDefault?: number;
+
+  /**
+   * 状态:正常、已删除 normal, deleted
+   */
+  status?: string;
+
+  /**
+   * 创建人
+   */
+  createdBy?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy?: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt?: string;
+
+  /**
+   * 地理位置编码
+   */
+  locationCode?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 75 - 0
src/api/system/physical/region/index.ts

@@ -0,0 +1,75 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { RegionVO, RegionForm, RegionQuery } from '@/api/system/physical/region/types';
+
+/**
+ * 查询省市区列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listRegion = (query?: RegionQuery): AxiosPromise<RegionVO[]> => {
+  return request({
+    url: '/physical/region/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询省市区详细
+ * @param id
+ */
+export const getRegion = (id: string | number): AxiosPromise<RegionVO> => {
+  return request({
+    url: '/physical/region/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增省市区
+ * @param data
+ */
+export const addRegion = (data: RegionForm) => {
+  return request({
+    url: '/physical/region',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改省市区
+ * @param data
+ */
+export const updateRegion = (data: RegionForm) => {
+  return request({
+    url: '/physical/region',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除省市区
+ * @param id
+ */
+export const delRegion = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/physical/region/' + id,
+    method: 'delete'
+  });
+};
+/**
+ * 查询城市列表
+ * @param parentCode 父级编码
+ * @param level 层级
+ */
+export const selectCityList = (parentCode: string, level: string): AxiosPromise<RegionVO[]> => {
+  return request({
+    url: '/physical/region/selectCityList',
+    method: 'get',
+    params: { parentCode, level }
+  });
+};

+ 80 - 0
src/api/system/physical/region/types.ts

@@ -0,0 +1,80 @@
+export interface RegionVO {
+  /**
+   *
+   */
+  id: string | number;
+
+  /**
+   * 行政区划代码
+   */
+  code: string;
+
+  /**
+   * 名称
+   */
+  name: string;
+
+  /**
+   * 父级代码
+   */
+  parentCode: string;
+
+  /**
+   * 1:省 2:市 3:区县
+   */
+  level: number;
+}
+
+export interface RegionForm extends BaseEntity {
+  /**
+   *
+   */
+  id?: string | number;
+
+  /**
+   * 行政区划代码
+   */
+  code?: string;
+
+  /**
+   * 名称
+   */
+  name?: string;
+
+  /**
+   * 父级代码
+   */
+  parentCode?: string;
+
+  /**
+   * 1:省 2:市 3:区县
+   */
+  level?: number;
+}
+
+export interface RegionQuery extends PageQuery {
+  /**
+   * 行政区划代码
+   */
+  code?: string;
+
+  /**
+   * 名称
+   */
+  name?: string;
+
+  /**
+   * 父级代码
+   */
+  parentCode?: string;
+
+  /**
+   * 1:省 2:市 3:区县
+   */
+  level?: number;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 222 - 18
src/views/system/physical/order/index.vue

@@ -91,20 +91,23 @@
         </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
-            <el-tooltip content="录入物流订单号" placement="top">
-              <el-button link type="success" icon="Edit" @click="handleInputWaybillNumber(scope.row)" v-hasPermi="['physical:order:edit']"
-                >录入单号</el-button
-              >
-            </el-tooltip>
-            <el-tooltip content="修改地址" placement="top">
-              <el-button link type="success" icon="Edit" v-hasPermi="['physical:order:edit']">修改地址</el-button>
-            </el-tooltip>
-            <el-tooltip content="发货" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleShipment(scope.row)" v-hasPermi="['physical:order:edit']">发货</el-button>
-            </el-tooltip>
-<!--            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['physical:order:remove']"></el-button>
-            </el-tooltip>-->
+            <div class="flex flex-col gap-1">
+              <el-tooltip content="录入物流订单号" placement="top">
+                <el-button link type="success" icon="Edit" @click="handleInputWaybillNumber(scope.row)" v-hasPermi="['physical:order:edit']">
+                  录入单号
+                </el-button>
+              </el-tooltip>
+              <el-tooltip content="修改地址" placement="top">
+                <el-button link type="success" icon="Edit" @click="handleEditAddress(scope.row)" v-hasPermi="['physical:order:edit']">
+                  修改地址
+                </el-button>
+              </el-tooltip>
+              <el-tooltip content="发货" placement="top">
+                <el-button link type="primary" icon="Edit" @click="handleShipment(scope.row)" v-hasPermi="['physical:order:edit']">
+                  发货
+                </el-button>
+              </el-tooltip>
+            </div>
           </template>
         </el-table-column>
       </el-table>
@@ -156,12 +159,58 @@
         </div>
       </template>
     </el-dialog>
+
+    <el-dialog title="修改收货地址" v-model="addressDialog.visible" width="500px" append-to-body>
+      <el-form ref="addressFormRef" :model="addressForm" label-width="100px">
+        <el-form-item label="省份" required>
+          <el-select v-model="addressForm.provinceName" placeholder="请选择省份" @change="handleProvinceChange" style="width: 100%">
+            <el-option v-for="item in provinces" :key="item.code" :label="item.name" :value="item.name" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="城市" required>
+          <el-select v-model="addressForm.cityName" placeholder="请选择城市" @change="handleCityChange" style="width: 100%">
+            <el-option v-for="item in cities" :key="item.code" :label="item.name" :value="item.name" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="区县" required>
+          <el-select v-model="addressForm.districtName" placeholder="请选择区县" style="width: 100%">
+            <el-option v-for="item in districts" :key="item.code" :label="item.name" :value="item.name" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="联系人" required>
+          <el-input v-model="addressForm.receiverName" placeholder="请输入联系人姓名" />
+        </el-form-item>
+
+        <el-form-item label="联系方式" required>
+          <el-input v-model="addressForm.mobilePhone" placeholder="请输入手机号码" />
+        </el-form-item>
+
+        <el-form-item label="详细地址" required>
+          <el-input v-model="addressForm.detailAddress" placeholder="请输入详细地址" type="textarea" :rows="3" />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitAddressForm">确 定</el-button>
+          <el-button @click="cancelAddress">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="Order" lang="ts">
 import { listOrder, getOrder, delOrder, addOrder, updateOrder, updateOrderSelective, sendShipment } from '@/api/system/physical/order';
+
 import { OrderVO, OrderQuery, OrderForm } from '@/api/system/physical/order/types';
+import { RegionVO } from '@/api/system/physical/region/types';
+import { selectCityList } from '@/api/system/physical/region';
+import { selectPhysicalOrderAddress, updateOrderAddress } from '@/api/system/physical/orderAddress';
+import { OrderAddressForm } from '@/api/system/physical/orderAddress/types';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const orderList = ref<OrderVO[]>([]);
@@ -172,7 +221,7 @@ const ids = ref<Array<string | number>>([]);
 const single = ref(true);
 const multiple = ref(true);
 const total = ref(0);
-
+const addressFormRef = ref<ElFormInstance>();
 const queryFormRef = ref<ElFormInstance>();
 const orderFormRef = ref<ElFormInstance>();
 const waybillFormRef = ref<ElFormInstance>();
@@ -342,8 +391,8 @@ const getOrderStatusText = (status: string): string => {
 };
 
 /** 获取订单状态标签类型 */
-const getOrderStatusType = (status: string): 'success' | 'warning' | 'info' | 'danger' | '' => {
-  const typeMap: Record<string, 'success' | 'warning' | 'info' | 'danger' | ''> = {
+const getOrderStatusType = (status: string): 'success' | 'warning' | 'info' | 'danger' | 'primary' | '' => {
+  const typeMap: Record<string, 'success' | 'warning' | 'info' | 'danger' | 'primary' | ''> = {
     pending_payment: 'warning',
     paid: 'success',
     shipped: 'success',
@@ -353,7 +402,6 @@ const getOrderStatusType = (status: string): 'success' | 'warning' | 'info' | 'd
   return typeMap[status] || '';
 };
 
-// ... existing code ...
 onMounted(() => {
   getList();
 });
@@ -411,4 +459,160 @@ const handleShipment = async (row: OrderVO) => {
     buttonLoading.value = false;
   }
 };
+
+const addressDialog = reactive({
+  visible: false,
+  currentOrderId: undefined as string | number | undefined,
+  orderSn: '',
+  addressId: undefined as string | number | undefined
+});
+
+const addressForm = reactive({
+  provinceName: '',
+  cityName: '',
+  districtName: '',
+  receiverName: '',
+  mobilePhone: '',
+  detailAddress: ''
+});
+
+const provinces = ref<RegionVO[]>([]);
+const cities = ref<RegionVO[]>([]);
+const districts = ref<RegionVO[]>([]);
+/** 修改地址按钮操作 */
+const handleEditAddress = async (row: OrderVO) => {
+  addressDialog.currentOrderId = row.id;
+  addressDialog.orderSn = row.orderSn;
+  addressForm.provinceName = '';
+  addressForm.cityName = '';
+  addressForm.districtName = '';
+  addressForm.receiverName = '';
+  addressForm.mobilePhone = '';
+  addressForm.detailAddress = '';
+
+  provinces.value = [];
+  cities.value = [];
+  districts.value = [];
+  await loadProvinces();
+  if (row.orderSn) {
+    try {
+      const res = await selectPhysicalOrderAddress(row.orderSn);
+      if (res.data) {
+        addressDialog.addressId = res.data.id;
+        addressForm.provinceName = res.data.provinceName || '';
+        addressForm.cityName = res.data.cityName || '';
+        addressForm.districtName = res.data.districtName || '';
+        addressForm.receiverName = res.data.receiverName || '';
+        addressForm.mobilePhone = res.data.mobilePhone || '';
+        addressForm.detailAddress = res.data.detailAddress || '';
+        if (addressForm.provinceName) {
+          const province = provinces.value.find((item) => item.name === addressForm.provinceName);
+          if (province) {
+            const cityRes = await selectCityList(province.code, '2');
+            cities.value = cityRes.data || [];
+            if (addressForm.cityName) {
+              const city = cities.value.find((item) => item.name === addressForm.cityName);
+              if (city) {
+                const districtRes = await selectCityList(city.code, '3');
+                districts.value = districtRes.data || [];
+              }
+            }
+          }
+        }
+      }
+    } catch (error) {
+      console.error('获取地址失败:', error);
+    }
+  }
+  addressDialog.visible = true;
+};
+
+/** 加载省份列表 */
+const loadProvinces = async () => {
+  const res = await selectCityList('', '1');
+  provinces.value = res.data || [];
+};
+
+/** 省份切换 */
+const handleProvinceChange = async (provinceName: string) => {
+  cities.value = [];
+  districts.value = [];
+  addressForm.cityName = '';
+  addressForm.districtName = '';
+
+  if (!provinceName) return;
+
+  const province = provinces.value.find((item) => item.name === provinceName);
+  if (province) {
+    const res = await selectCityList(province.code, '2');
+    cities.value = res.data || [];
+  }
+};
+
+/** 城市切换 */
+const handleCityChange = async (cityName: string) => {
+  districts.value = [];
+  addressForm.districtName = '';
+
+  if (!cityName) return;
+
+  const city = cities.value.find((item) => item.name === cityName);
+  if (city) {
+    const res = await selectCityList(city.code, '3');
+    districts.value = res.data || [];
+  }
+};
+
+/** 取消修改地址 */
+const cancelAddress = () => {
+  addressDialog.visible = false;
+  addressDialog.currentOrderId = undefined;
+  addressFormRef.value?.resetFields();
+};
+
+/** 提交修改地址 */
+const submitAddressForm = async () => {
+  if (!addressForm.provinceName || !addressForm.cityName || !addressForm.districtName) {
+    proxy?.$modal.msgError('请选择省市区');
+    return;
+  }
+  if (!addressForm.receiverName) {
+    proxy?.$modal.msgError('请输入联系人姓名');
+    return;
+  }
+  if (!addressForm.mobilePhone) {
+    proxy?.$modal.msgError('请输入联系方式');
+    return;
+  }
+  if (!addressForm.detailAddress) {
+    proxy?.$modal.msgError('请输入详细地址');
+    return;
+  }
+  buttonLoading.value = true;
+  try {
+    const fullAddress = `${addressForm.provinceName}${addressForm.cityName}${addressForm.districtName}${addressForm.detailAddress}`;
+    await updateOrderAddress({
+      orderSn: addressDialog.orderSn,
+      provinceName: addressForm.provinceName,
+      provinceCode: provinces.value.find((p) => p.name === addressForm.provinceName)?.code || '',
+      cityName: addressForm.cityName,
+      cityCode: cities.value.find((c) => c.name === addressForm.cityName)?.code || '',
+      districtName: addressForm.districtName,
+      districtCode: districts.value.find((d) => d.name === addressForm.districtName)?.code || '',
+      receiverName: addressForm.receiverName,
+      mobilePhone: addressForm.mobilePhone,
+      detailAddress: addressForm.detailAddress,
+      userAddress: fullAddress,
+      status: 'normal',
+      id: addressDialog.addressId,
+      currentOrderId: addressDialog.currentOrderId
+    } as OrderAddressForm);
+    proxy?.$modal.msgSuccess('地址修改成功');
+    addressDialog.visible = false;
+    addressDialog.currentOrderId = undefined;
+    await getList();
+  } finally {
+    buttonLoading.value = false;
+  }
+};
 </script>