Bladeren bron

feat(订单表单): 添加地址选择组件及关联逻辑

yz 2 weken geleden
bovenliggende
commit
0da12a356a

+ 459 - 0
src/components/order-form/address-select.vue

@@ -0,0 +1,459 @@
+<template>
+  <el-select
+    v-model="currentValue"
+    :placeholder="placeholder"
+    :disabled="disabled"
+    :clearable="clearable"
+    :filterable="false"
+    :loading="loading"
+    :size="size"
+    :class="selectClass"
+    @change="handleChange"
+    @clear="handleClear"
+    @focus="handleFocus"
+    @blur="handleBlur"
+  >
+    <el-option
+      v-for="item in addressOptions"
+      :key="item.id"
+      :label="item.label"
+      :value="item.id"
+    >
+      <span style="float: left">{{ item.receiverName }}</span>
+      <span style="float: right; color: #8492a6; font-size: 13px">{{ item.receiverPhone }}</span>
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { getList } from '@/api/order/address'
+
+/**
+ * @typedef {import('@/api/types/address').CustomerAddressRecord} CustomerAddressRecord
+ * @typedef {import('@/api/types/address').CustomerAddressQueryParams} CustomerAddressQueryParams
+ * @typedef {import('@/api/types/address').CustomerAddressListResponse} CustomerAddressListResponse
+ */
+
+/**
+ * 地址选择器选项接口
+ * @typedef {Object} AddressSelectOption
+ * @property {string|number} id - 地址唯一标识
+ * @property {string} label - 显示标签
+ * @property {string} receiverName - 收货人姓名
+ * @property {string} receiverPhone - 收货人电话
+ * @property {string} regionCode - 地区编码
+ * @property {string} regionName - 地区名称
+ * @property {string} detailAddress - 详细地址
+ * @property {string} postalCode - 邮政编码
+ * @property {0|1} isDefault - 是否默认地址
+ * @property {CustomerAddressRecord} addressData - 完整地址数据
+ */
+
+/**
+ * 地址回显信息接口
+ * @typedef {Object} AddressEchoInfo
+ * @property {string} receiverName - 收货人姓名
+ * @property {string} receiverPhone - 收货人电话
+ * @property {string} regionName - 地区名称
+ * @property {string} detailAddress - 详细地址
+ */
+
+/**
+ * 地址选择事件数据接口
+ * @typedef {Object} AddressSelectedData
+ * @property {string|number} addressId - 地址ID
+ * @property {string} receiverName - 收货人姓名
+ * @property {string} receiverPhone - 收货人电话
+ * @property {string} regionCode - 地区编码
+ * @property {string} regionName - 地区名称
+ * @property {string} detailAddress - 详细地址
+ * @property {string} postalCode - 邮政编码
+ * @property {CustomerAddressRecord} [addressData] - 完整地址数据
+ */
+
+/**
+ * 地址选择组件
+ * @description 基于Element UI Select的地址选择组件,根据客户编码动态加载地址列表
+ * @component
+ */
+export default {
+  name: 'AddressSelect',
+  
+  /**
+   * 组件属性定义
+   * @description 定义组件接收的外部属性及其类型约束
+   */
+  props: {
+    /**
+     * 绑定值
+     * @description 当前选中的地址ID
+     * @type {string|number}
+     * @default ''
+     */
+    value: {
+      type: [String, Number],
+      default: '',
+      validator: (value) => value === '' || value === null || value === undefined || typeof value === 'string' || typeof value === 'number'
+    },
+    /**
+     * 客户编码
+     * @description 用于筛选地址的客户编码,当此值变化时会重新加载地址列表
+     * @type {string}
+     * @default ''
+     */
+    customerCode: {
+      type: String,
+      default: '',
+      validator: (value) => typeof value === 'string'
+    },
+    /**
+     * 占位符文本
+     * @description 输入框的占位符提示文本
+     * @type {string}
+     * @default '请选择收货地址'
+     */
+    placeholder: {
+      type: String,
+      default: '请选择收货地址',
+      validator: (value) => typeof value === 'string'
+    },
+    /**
+     * 是否禁用
+     * @description 控制组件是否处于禁用状态
+     * @type {boolean}
+     * @default false
+     */
+    disabled: {
+      type: Boolean,
+      default: false,
+      validator: (value) => typeof value === 'boolean'
+    },
+    /**
+     * 是否可清空
+     * @description 控制是否显示清空按钮
+     * @type {boolean}
+     * @default true
+     */
+    clearable: {
+      type: Boolean,
+      default: true,
+      validator: (value) => typeof value === 'boolean'
+    },
+    /**
+     * 组件尺寸
+     * @description 控制组件的显示尺寸
+     * @type {'large'|'medium'|'small'|'mini'}
+     * @default 'small'
+     */
+    size: {
+      type: String,
+      default: 'small',
+      validator: (value) => ['large', 'medium', 'small', 'mini'].includes(value)
+    }
+  },
+  
+  /**
+   * 组件响应式数据
+   * @description 定义组件的内部状态数据
+   * @returns {Object} 组件数据对象
+   */
+  data() {
+    return {
+      /**
+       * 当前选中值
+       * @type {string|number}
+       */
+      currentValue: this.value,
+      /**
+       * 地址选项列表
+       * @type {AddressSelectOption[]}
+       */
+      addressOptions: [],
+      /**
+       * 加载状态
+       * @type {boolean}
+       */
+      loading: false
+    }
+  },
+  
+  /**
+   * 计算属性
+   * @description 组件的响应式计算属性
+   */
+  computed: {
+    /**
+     * 选择器样式类
+     * @description 返回与Avue组件一致的样式类名
+     * @returns {Object} 样式类对象
+     */
+    selectClass() {
+      return {
+        'avue-select': true,
+        'el-select': true
+      }
+    }
+  },
+  
+  /**
+   * 监听器
+   * @description 监听属性变化并执行相应的处理逻辑
+   */
+  watch: {
+    /**
+     * 监听value属性变化
+     * @description 当外部传入的value发生变化时,同步更新内部状态
+     * @param {string|number} newVal - 新的地址ID值
+     * @param {string|number} oldVal - 旧的地址ID值
+     */
+    value(newVal, oldVal) {
+      if (newVal !== oldVal) {
+        this.currentValue = newVal
+      }
+    },
+    /**
+     * 监听currentValue变化
+     * @description 当内部值发生变化时,向父组件发送input事件
+     * @param {string|number} newVal - 新的值
+     */
+    currentValue(newVal) {
+      this.$emit('input', newVal)
+    },
+    /**
+     * 监听customerCode变化
+     * @description 当客户编码变化时,重新加载地址列表并清空当前选择
+     * @param {string} newVal - 新的客户编码
+     * @param {string} oldVal - 旧的客户编码
+     */
+    customerCode(newVal, oldVal) {
+      if (newVal !== oldVal) {
+        this.currentValue = ''
+        this.loadAddressList(newVal)
+      }
+    }
+  },
+  
+  /**
+   * 组件挂载钩子
+   * @description 组件挂载完成后执行的初始化逻辑
+   */
+  mounted() {
+    // 组件挂载时,如果有客户编码,加载对应的地址列表
+    if (this.customerCode) {
+      this.loadAddressList(this.customerCode)
+    }
+  },
+  
+  /**
+   * 组件方法
+   * @description 组件的业务逻辑方法集合
+   */
+  methods: {
+    /**
+     * 加载地址列表
+     * @description 根据客户编码异步加载地址列表数据
+     * @param {string} customerCode - 客户编码
+     * @returns {Promise<void>}
+     * @throws {Error} 当API调用失败时抛出异常
+     */
+    async loadAddressList(customerCode) {
+      if (!customerCode || typeof customerCode !== 'string') {
+        this.addressOptions = []
+        return
+      }
+      
+      try {
+        this.loading = true
+        
+        /** @type {CustomerAddressQueryParams} */
+        const queryParams = {
+          customerCode: customerCode
+        }
+        
+        /** @type {CustomerAddressListResponse} */
+        const response = await getList(1, 100, queryParams)
+        
+        if (response?.data?.success && response.data.data?.records) {
+          this.addressOptions = this.transformAddressData(response.data.data.records)
+        } else {
+          this.addressOptions = []
+          const errorMsg = response?.data?.msg || '获取地址列表失败'
+          console.warn('获取地址列表失败:', errorMsg)
+        }
+      } catch (error) {
+        console.error('加载地址列表API调用失败:', error)
+        this.addressOptions = []
+        this.$message.error('网络错误,加载地址列表失败')
+        throw error
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    /**
+     * 转换地址数据
+     * @description 将API返回的地址数据转换为组件所需的选项格式
+     * @param {CustomerAddressRecord[]} addressRecords - 地址记录数组
+     * @returns {AddressSelectOption[]} 转换后的地址选项数组
+     * @private
+     */
+    transformAddressData(addressRecords) {
+      return addressRecords.map(address => ({
+        id: address.id,
+        label: `${address.receiverName} - ${address.receiverPhone} - ${address.regionName}${address.detailAddress}`,
+        receiverName: address.receiverName,
+        receiverPhone: address.receiverPhone,
+        regionCode: address.regionCode,
+        regionName: address.regionName,
+        detailAddress: address.detailAddress,
+        postalCode: address.postalCode || '',
+        isDefault: address.isDefault,
+        addressData: address
+      }))
+    },
+    
+    /**
+     * 设置回显值
+     * @description 在编辑模式下,根据地址信息匹配并设置选中的地址
+     * @param {AddressEchoInfo} addressInfo - 地址信息对象
+     * @returns {void}
+     */
+    setEchoValue(addressInfo) {
+      if (!this.customerCode || !addressInfo) {
+        return
+      }
+      
+      // 如果地址列表为空,先加载数据
+      if (this.addressOptions.length === 0) {
+        this.loadAddressList(this.customerCode).then(() => {
+          this.matchAndSetAddress(addressInfo)
+        }).catch(error => {
+          console.error('加载地址列表失败:', error)
+        })
+      } else {
+        this.matchAndSetAddress(addressInfo)
+      }
+    },
+    
+    /**
+     * 匹配并设置地址
+     * @description 根据地址信息匹配地址列表中的项目并设置选中值
+     * @param {AddressEchoInfo} addressInfo - 地址信息对象
+     * @returns {void}
+     * @private
+     */
+    matchAndSetAddress(addressInfo) {
+      /** @type {AddressSelectOption|undefined} */
+      const matchedAddress = this.addressOptions.find(address => {
+        return address.receiverName === addressInfo.receiverName &&
+               address.receiverPhone === addressInfo.receiverPhone &&
+               address.regionName === addressInfo.regionName &&
+               address.detailAddress === addressInfo.detailAddress
+      })
+      
+      if (matchedAddress) {
+        this.currentValue = matchedAddress.id
+        // 触发选择事件,确保父组件状态同步
+        this.handleChange(matchedAddress.id)
+      }
+    },
+    
+    /**
+     * 选择变化处理
+     * @description 当用户选择地址时触发,更新内部状态并向父组件发送相关事件
+     * @param {string|number} value - 选中的地址ID
+     * @returns {void}
+     */
+    handleChange(value) {
+      this.currentValue = value
+      this.$emit('change', value)
+      
+      /** @type {AddressSelectOption|undefined} */
+      const selectedAddress = this.addressOptions.find(item => item.id == value)
+      
+      if (selectedAddress) {
+        /** @type {AddressSelectedData} */
+        const addressData = {
+          addressId: selectedAddress.id,
+          receiverName: selectedAddress.receiverName,
+          receiverPhone: selectedAddress.receiverPhone,
+          regionCode: selectedAddress.regionCode,
+          regionName: selectedAddress.regionName,
+          detailAddress: selectedAddress.detailAddress,
+          postalCode: selectedAddress.postalCode,
+          addressData: selectedAddress.addressData
+        }
+        
+        this.$emit('address-selected', addressData)
+      } else {
+        // 如果没有找到对应的地址,发送空数据
+        this.emitEmptyAddressData()
+      }
+    },
+    
+    /**
+     * 发送空地址数据
+     * @description 发送空的地址选择事件数据
+     * @returns {void}
+     * @private
+     */
+    emitEmptyAddressData() {
+      /** @type {AddressSelectedData} */
+      const emptyData = {
+        addressId: '',
+        receiverName: '',
+        receiverPhone: '',
+        regionCode: '',
+        regionName: '',
+        detailAddress: '',
+        postalCode: ''
+      }
+      
+      this.$emit('address-selected', emptyData)
+    },
+    
+    /**
+     * 清空处理
+     * @description 清空选择并发送相应事件
+     * @returns {void}
+     */
+    handleClear() {
+      this.currentValue = ''
+      this.$emit('clear')
+      this.emitEmptyAddressData()
+    },
+    
+    /**
+     * 获得焦点处理
+     * @description 组件获得焦点时的处理逻辑
+     * @returns {void}
+     */
+    handleFocus() {
+      this.$emit('focus')
+    },
+    
+    /**
+     * 失去焦点处理
+     * @description 组件失去焦点时的处理逻辑
+     * @returns {void}
+     */
+    handleBlur() {
+      this.$emit('blur')
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* 确保样式与Avue select保持一致 */
+.avue-select {
+  width: 100%;
+}
+
+/* 选项样式优化 */
+.el-select-dropdown__item {
+  height: auto;
+  line-height: 1.5;
+  padding: 8px 20px;
+}
+</style>

+ 13 - 0
src/components/order-form/form-option.js

@@ -202,6 +202,19 @@ export const orderFormOption = {
       prop: 'receiver',
       column: [
         {
+          label: '收货地址',
+          prop: 'addressId',
+          type: 'select',
+          span: 8,
+          slot: true,
+          placeholder: '请选择收货地址',
+          rules: [{
+            required: false,
+            message: '请选择收货地址',
+            trigger: 'change'
+          }]
+        },
+        {
           label: '收货人姓名',
           prop: 'receiverName',
           type: 'input',

+ 3 - 0
src/components/order-form/order-form-mixin.js

@@ -348,11 +348,13 @@ export default {
         orderCode: '',
         orgCode: '',
         orgName: '',
+        customerId: null,
         customerCode: '',
         customerName: '',
         orderType: ORDER_TYPES.NORMAL,
         totalAmount: null,
         totalQuantity: null,
+        addressId: '',
         receiverName: '',
         receiverPhone: '',
         receiverRegion: '',
@@ -589,6 +591,7 @@ export default {
         orderQuantity: orderQuantityValidation.isValid ? parseInt(orderQuantityValidation.value) : null,
         totalAmount: totalAmountValidation.isValid ? preciseRound(totalAmountValidation.value, 2) : null,
         totalQuantity: totalQuantityValidation.isValid ? preciseRound(totalQuantityValidation.value, 4) : null,
+        addressId: String(orderData.addressId || ''),
         receiverName: String(orderData.receiverName || ''),
         receiverPhone: String(orderData.receiverPhone || ''),
         receiverRegion: String(orderData.receiverRegion || ''),

+ 85 - 2
src/components/order-form/order-form.vue

@@ -47,6 +47,17 @@
             @customer-selected="handleCustomerSelected"
           />
         </template>
+        
+        <!-- 自定义地址选择组件 -->
+        <template #addressId="{ value, column }">
+          <address-select
+            v-model="formData.addressId"
+            :customer-code="formData.customerCode"
+            :placeholder="column.placeholder"
+            :disabled="column.disabled"
+            @address-selected="handleAddressSelected"
+          />
+        </template>
       </avue-form>
 
       <!-- 物料明细区域 -->
@@ -70,6 +81,7 @@ import orderFormMixin from './order-form-mixin'
 import { getFormOption } from './form-option'
 import MaterialDetailTable from './material-detail-table.vue'
 import CustomerSelect from './customer-select.vue'
+import AddressSelect from './address-select.vue'
 
 /**
  * @typedef {import('./types').OrderFormModel} OrderFormModel
@@ -91,7 +103,8 @@ export default {
    */
   components: {
     MaterialDetailTable,
-    CustomerSelect
+    CustomerSelect,
+    AddressSelect
   },
 
   /**
@@ -211,6 +224,21 @@ export default {
         }
       },
       immediate: true
+    },
+    
+    /**
+     * 监听表单数据变化,用于地址回显
+     * @description 当表单数据中的客户编码和地址相关字段都有值时,触发地址选择组件的回显
+     */
+    'formData.customerCode': {
+      handler(newVal) {
+        if (newVal && this.isEdit && this.formData.addressId) {
+          // 延迟执行,确保地址选择组件已经加载完成
+          this.$nextTick(() => {
+            this.handleAddressEcho()
+          })
+        }
+      }
     }
   },
 
@@ -315,7 +343,7 @@ export default {
 
     /**
      * 处理客户选择事件
-     * @description 当客户选择组件选择客户时的回调处理,自动填充客户编码和客户名称
+     * @description 当客户选择组件选择客户时的回调处理,自动填充客户编码和客户名称,并清空地址相关字段
      * @param {Object} customerData - 客户数据对象
      * @param {string|number} customerData.customerId - 客户ID
      * @param {string} customerData.customerCode - 客户编码
@@ -328,6 +356,61 @@ export default {
         this.$set(this.formData, 'customerId', customerData.customerId)
         this.$set(this.formData, 'customerCode', customerData.customerCode)
         this.$set(this.formData, 'customerName', customerData.customerName)
+        
+        // 清空地址相关字段
+        this.$set(this.formData, 'addressId', '')
+        this.$set(this.formData, 'receiverName', '')
+        this.$set(this.formData, 'receiverPhone', '')
+        this.$set(this.formData, 'receiverRegion', '')
+        this.$set(this.formData, 'receiverAddress', '')
+      }
+    },
+    
+    /**
+     * 处理地址选择事件
+     * @description 当地址选择组件选择地址时的回调处理,自动填充收货人相关信息
+     * @param {Object} addressData - 地址数据对象
+     * @param {string|number} addressData.addressId - 地址ID
+     * @param {string} addressData.receiverName - 收货人姓名
+     * @param {string} addressData.receiverPhone - 收货人电话
+     * @param {string} addressData.regionCode - 地区编码
+     * @param {string} addressData.regionName - 地区名称
+     * @param {string} addressData.detailAddress - 详细地址
+     * @param {string} addressData.postalCode - 邮政编码
+     * @returns {void}
+     */
+    handleAddressSelected(addressData) {
+      if (this.formData) {
+        // 更新地址相关字段
+        this.$set(this.formData, 'addressId', addressData.addressId)
+        this.$set(this.formData, 'receiverName', addressData.receiverName || '')
+        this.$set(this.formData, 'receiverPhone', addressData.receiverPhone || '')
+        this.$set(this.formData, 'receiverRegion', addressData.regionName || '')
+        this.$set(this.formData, 'receiverAddress', addressData.detailAddress || '')
+      }
+    },
+    
+    /**
+     * 处理地址回显
+     * @description 在编辑模式下,根据表单中的地址信息在地址选择组件中进行回显
+     * @returns {void}
+     */
+    handleAddressEcho() {
+      // 查找地址选择组件的引用
+      const addressSelectRefs = this.$refs.orderForm?.$refs?.addressId
+      const addressSelectComponent = Array.isArray(addressSelectRefs) ? addressSelectRefs[0] : addressSelectRefs
+      
+      if (addressSelectComponent && typeof addressSelectComponent.setEchoValue === 'function') {
+        // 构建地址信息对象用于匹配
+        const addressInfo = {
+          receiverName: this.formData.receiverName,
+          receiverPhone: this.formData.receiverPhone,
+          regionName: this.formData.receiverRegion,
+          detailAddress: this.formData.receiverAddress
+        }
+        
+        // 调用地址选择组件的回显方法
+        addressSelectComponent.setEchoValue(addressInfo)
       }
     }
   }