Sfoglia il codice sorgente

feat(订单表单): 添加客户选择组件并完善相关功能

yz 1 mese fa
parent
commit
2d4e0d338b

+ 2 - 33
src/api/common/customer.js

@@ -2,11 +2,8 @@ import request from '@/router/axios';
 
 /**
  * 获取客户列表
- * @param {Object} params - 查询参数
- * @param {number} params.size - 每页条数
- * @param {number} params.current - 当前页码
- * @param {string} [params.customerName] - 客户名称筛选
- * @returns {Promise<AxiosResponse<CustomerResponse>>} 分页客户数据
+ * @param {import('../types/customer').CustomerQueryParams} params - 查询参数
+ * @returns {Promise<import('../types/customer').CustomerListResponse>} 客户列表响应
  */
 export const getCustomerList = async (params) => {
     return request({
@@ -20,31 +17,3 @@ export const getCustomerList = async (params) => {
         }
     });
 };
-
-/**
- * @typedef {Object} CustomerRecord
- * @property {string} id - 客户ID
- * @property {number} ORG_ID - 组织ID
- * @property {number} Customer_ID - 客户ID
- * @property {string} Customer_CODE - 客户编码
- * @property {string} Customer_NAME - 客户名称
- * @property {string} Customer_ShortName - 客户简称
- * @property {string} TaxSchedule_NAME - 税率名称
- * @property {number} TaxRate - 税率
- * @property {string} TradeCurrency_NAME - 交易币种
- * @property {string} ShippmentRule_NAME - 出货规则
- * @property {string} RecervalTerm_NAME - 收款条件
- * @property {string} CreatedOn - 创建时间
- */
-
-/**
- * @typedef {Object} CustomerResponse
- * @property {number} code - 状态码
- * @property {boolean} success - 请求状态
- * @property {Object} data - 响应数据
- * @property {CustomerRecord[]} data.records - 客户记录
- * @property {number} data.total - 总记录数
- * @property {number} data.size - 每页大小
- * @property {number} data.current - 当前页码
- * @property {string} msg - 消息描述
- */

+ 60 - 0
src/api/types/customer.d.ts

@@ -0,0 +1,60 @@
+import { AxiosResponse } from 'axios';
+
+/**
+ * 基础查询参数接口
+ */
+export interface BaseQueryParams {
+  current?: number;
+  size?: number;
+}
+
+/**
+ * 分页结果接口
+ */
+export interface PageResult<T> {
+  records: T[];
+  total: number;
+  size: number;
+  current: number;
+  pages: number;
+}
+
+/**
+ * API响应数据接口
+ */
+export interface ApiResponseData<T> {
+  code: number;
+  msg: string;
+  success: boolean;
+  data: T;
+}
+
+/**
+ * 客户查询参数接口
+ */
+export interface CustomerQueryParams extends BaseQueryParams {
+  customerName?: string;
+}
+
+/**
+ * 客户记录接口
+ */
+export interface CustomerRecord {
+  id: string;
+  ORG_ID: number;
+  Customer_ID: number;
+  Customer_CODE: string;
+  Customer_NAME: string;
+  Customer_ShortName: string;
+  TaxSchedule_NAME: string;
+  TaxRate: number;
+  TradeCurrency_NAME: string;
+  ShippmentRule_NAME: string;
+  RecervalTerm_NAME: string;
+  CreatedOn: string;
+}
+
+/**
+ * 客户列表响应接口
+ */
+export type CustomerListResponse = AxiosResponse<ApiResponseData<PageResult<CustomerRecord>>>;

+ 400 - 0
src/components/order-form/customer-select.vue

@@ -0,0 +1,400 @@
+<template>
+  <el-select
+    v-model="currentValue"
+    :placeholder="placeholder"
+    :disabled="disabled"
+    :clearable="clearable"
+    :filterable="true"
+    :remote="true"
+    :remote-method="remoteSearch"
+    :loading="loading"
+    :size="size"
+    :class="selectClass"
+    @change="handleChange"
+    @clear="handleClear"
+    @focus="handleFocus"
+    @blur="handleBlur"
+  >
+    <el-option
+      v-for="item in options"
+      :key="item.Customer_ID"
+      :label="item.Customer_NAME"
+      :value="item.Customer_ID"
+    >
+      <span style="float: left">{{ item.Customer_NAME }}</span>
+      <span style="float: right; color: #8492a6; font-size: 13px">{{ item.Customer_CODE }}</span>
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { getCustomerList } from '@/api/common/customer'
+
+/**
+ * @typedef {import('@/api/types/customer').CustomerRecord} CustomerRecord
+ * @typedef {import('@/api/types/customer').CustomerQueryParams} CustomerQueryParams
+ * @typedef {import('@/api/types/customer').CustomerListResponse} CustomerListResponse
+ */
+
+/**
+ * 客户选择组件
+ * @description 基于Element UI Select的客户选择组件,支持远程搜索和数据回显
+ * @component
+ */
+export default {
+  name: 'CustomerSelect',
+  
+  /**
+   * 组件属性定义
+   * @description 定义组件接收的外部属性
+   */
+  props: {
+    /**
+     * 绑定值
+     * @description 当前选中的客户ID
+     */
+    value: {
+      type: [String, Number],
+      default: ''
+    },
+    /**
+     * 占位符文本
+     * @description 输入框的占位符提示文本
+     */
+    placeholder: {
+      type: String,
+      default: '请选择客户'
+    },
+    /**
+     * 是否禁用
+     * @description 控制组件是否处于禁用状态
+     */
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 是否可清空
+     * @description 控制是否显示清空按钮
+     */
+    clearable: {
+      type: Boolean,
+      default: true
+    },
+    /**
+     * 组件尺寸
+     * @description 控制组件的显示尺寸
+     */
+    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 {CustomerRecord[]}
+       */
+      options: [],
+      /**
+       * 加载状态
+       * @type {boolean}
+       */
+      loading: false,
+      /**
+       * 搜索防抖定时器
+       * @type {number|null}
+       */
+      searchTimer: null
+    }
+  },
+  
+  /**
+   * 计算属性
+   * @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
+        // 当value变化时,如果有值且当前选项为空,则加载对应的客户信息
+        if (newVal && this.options.length === 0) {
+          this.loadCustomerById(newVal)
+        }
+      }
+    },
+    /**
+     * 监听currentValue变化
+     * @description 当内部值发生变化时,向父组件发送input事件
+     * @param {string|number} newVal - 新的值
+     */
+    currentValue(newVal) {
+      this.$emit('input', newVal)
+    }
+  },
+  
+  /**
+   * 组件挂载钩子
+   * @description 组件挂载完成后执行的初始化逻辑
+   */
+  mounted() {
+    // 组件挂载时,如果有初始值,加载对应的客户信息
+    if (this.value) {
+      this.loadCustomerById(this.value)
+    }
+  },
+  
+  /**
+   * 组件销毁钩子
+   * @description 组件销毁前清理定时器等资源
+   */
+  beforeDestroy() {
+    if (this.searchTimer) {
+      clearTimeout(this.searchTimer)
+      this.searchTimer = null
+    }
+  },
+  
+  /**
+   * 组件方法
+   * @description 组件的业务逻辑方法集合
+   */
+  methods: {
+    /**
+     * 远程搜索方法
+     * @description 实现防抖的远程搜索功能,避免频繁的API调用
+     * @param {string} query - 搜索关键词
+     * @returns {Promise<void>}
+     */
+    async remoteSearch(query) {
+      // 清除之前的定时器
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer)
+        this.searchTimer = null
+      }
+      
+      // 如果查询为空,清空选项
+      if (!query || typeof query !== 'string' || query.trim() === '') {
+        this.options = []
+        return
+      }
+      
+      // 设置防抖,避免频繁请求
+      this.searchTimer = setTimeout(async () => {
+        try {
+          await this.searchCustomers(query.trim())
+        } catch (error) {
+          console.error('远程搜索失败:', error)
+        }
+      }, 300)
+    },
+    
+    /**
+     * 搜索客户
+     * @description 根据关键词搜索客户列表
+     * @param {string} keyword - 搜索关键词
+     * @returns {Promise<void>}
+     * @throws {Error} 当API调用失败时抛出异常
+     */
+    async searchCustomers(keyword) {
+      if (!keyword || typeof keyword !== 'string') {
+        return
+      }
+      
+      try {
+        this.loading = true
+        
+        /** @type {CustomerQueryParams} */
+        const queryParams = {
+          current: 1,
+          size: 20,
+          customerName: keyword
+        }
+        
+        /** @type {CustomerListResponse} */
+        const response = await getCustomerList(queryParams)
+        
+        if (response?.data?.success && response.data.data?.records) {
+          this.options = response.data.data.records
+        } else {
+          this.options = []
+          const errorMsg = response?.data?.msg || '搜索客户失败'
+          this.$message.warning(errorMsg)
+        }
+      } catch (error) {
+        console.error('搜索客户API调用失败:', error)
+        this.options = []
+        this.$message.error('网络错误,搜索客户失败')
+        throw error
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    /**
+     * 根据ID加载客户信息
+     * @description 通过客户ID查询并加载单个客户的详细信息,用于数据回显
+     * @param {string|number} customerId - 客户ID
+     * @returns {Promise<void>}
+     * @throws {Error} 当API调用失败时抛出异常
+     */
+    async loadCustomerById(customerId) {
+      if (!customerId) {
+        return
+      }
+      
+      try {
+        this.loading = true
+        
+        /** @type {CustomerQueryParams} */
+        const queryParams = {
+          current: 1,
+          size: 200, // 增加查询范围以确保找到目标客户
+          customerName: '' // 空字符串获取所有客户
+        }
+        
+        /** @type {CustomerListResponse} */
+        const response = await getCustomerList(queryParams)
+        
+        if (response?.data?.success && response.data.data?.records) {
+          const allCustomers = response.data.data.records
+          /** @type {CustomerRecord|undefined} */
+          const customer = allCustomers.find(item => item.Customer_ID == customerId)
+          
+          if (customer) {
+            // 只保留匹配的客户选项
+            this.options = [customer]
+          } else {
+            // 如果在当前页没找到,可能需要查询更多页面
+            console.warn(`未找到客户ID为 ${customerId} 的客户信息`)
+            this.options = []
+          }
+        } else {
+          const errorMsg = response?.data?.msg || '获取客户列表失败'
+          console.error('获取客户列表失败:', errorMsg)
+          this.options = []
+          this.$message.warning(errorMsg)
+        }
+      } catch (error) {
+        console.error('加载客户信息API调用失败:', error)
+        this.options = []
+        this.$message.error('网络错误,加载客户信息失败')
+        throw error
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    /**
+     * 选择变化处理
+     * @description 当用户选择客户时触发,更新内部状态并向父组件发送相关事件
+     * @param {string|number} value - 选中的客户ID
+     * @returns {void}
+     */
+    handleChange(value) {
+      this.currentValue = value
+      this.$emit('change', value)
+      
+      // 查找选中的客户对象
+      /** @type {CustomerRecord|undefined} */
+      const selectedCustomer = this.options.find(item => item.Customer_ID == value)
+      
+      if (selectedCustomer) {
+        // 发送客户选择事件,包含完整的客户信息
+        this.$emit('customer-selected', {
+          customerId: selectedCustomer.Customer_ID,
+          customerCode: selectedCustomer.Customer_CODE,
+          customerName: selectedCustomer.Customer_NAME,
+          customerData: selectedCustomer // 传递完整的客户对象
+        })
+      } else {
+        // 如果没有找到对应的客户,发送空数据
+        this.$emit('customer-selected', {
+          customerId: '',
+          customerCode: '',
+          customerName: '',
+          customerData: null
+        })
+      }
+    },
+    
+    /**
+     * 清空处理
+     */
+    handleClear() {
+      this.currentValue = ''
+      this.options = []
+      this.$emit('clear')
+      this.$emit('customer-selected', {
+        customerId: '',
+        customerCode: '',
+        customerName: ''
+      })
+    },
+    
+    /**
+     * 获得焦点处理
+     */
+    handleFocus() {
+      this.$emit('focus')
+    },
+    
+    /**
+     * 失去焦点处理
+     */
+    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

@@ -101,6 +101,19 @@ export const orderFormOption = {
           }]
         },
         {
+          label: '客户',
+          prop: 'customerId',
+          type: 'select',
+          span: 8,
+          slot: true,
+          placeholder: '请选择客户',
+          rules: [{
+            required: true,
+            message: '请选择客户',
+            trigger: 'change'
+          }]
+        },
+        {
           label: '客户编码',
           prop: 'customerCode',
           type: 'input',

+ 187 - 56
src/components/order-form/order-form-mixin.js

@@ -1,6 +1,14 @@
+/**
+ * @fileoverview 订单表单混入组件
+ * @description 提供订单表单的数据管理、验证规则和业务逻辑的混入组件,支持新增和编辑模式
+ */
+
+// API接口导入
 import { add, update, getDetail } from '@/api/order/order'
 import { getList as getOrderItemList } from '@/api/order/order-item'
 import { createSalesOrder } from '@/api/order/sales-order'
+
+// 常量和枚举导入
 import {
   ORDER_TYPES,
   ORDER_STATUS,
@@ -8,12 +16,12 @@ import {
   ORDER_STATUS_OPTIONS
 } from '@/constants/order'
 
-// 导入本地常量定义
+// 本地常量定义导入
 import {
   MaterialDetailDataSource
 } from './constants'
 
-// 导入数字格式化工具
+// 数字格式化工具导入
 import {
   formatAmount,
   formatFloatNumber,
@@ -28,17 +36,63 @@ import {
 } from './number-format-utils'
 
 /**
+ * 类型定义导入
+ * @description 导入所有必要的TypeScript类型定义,确保类型安全
+ */
+
+/**
  * @typedef {import('./constants').MaterialDetailRecord} MaterialDetailRecord
+ * @description 物料明细记录类型
+ */
+
+/**
  * @typedef {import('./constants').OrderFormModel} OrderFormModel
+ * @description 订单表单数据模型类型
+ */
+
+/**
  * @typedef {import('./constants').MaterialDeleteEventData} MaterialDeleteEventData
+ * @description 物料删除事件数据类型
+ */
+
+/**
  * @typedef {import('./constants').ApiResponse} ApiResponse
+ * @description API响应数据类型
+ */
+
+/**
  * @typedef {import('./constants').PaginatedResponse} PaginatedResponse
+ * @description 分页响应数据类型
+ */
+
+/**
  * @typedef {import('./constants').ValidationRule} ValidationRule
+ * @description 表单验证规则类型
+ */
+
+/**
  * @typedef {import('./constants').OrderFormMixinData} OrderFormMixinData
+ * @description 订单表单混入数据类型
+ */
+
+/**
  * @typedef {import('@/api/types/order').SalesOrderCreateForm} SalesOrderCreateForm
+ * @description 销售订单创建表单类型
+ */
+
+/**
  * @typedef {import('@/api/types/order').SalesOrderItemCreateForm} SalesOrderItemCreateForm
+ * @description 销售订单明细创建表单类型
+ */
+
+/**
  * @typedef {import('@/constants/order').ORDER_TYPES} OrderTypeValue
+ * @description 订单类型枚举值类型
+ */
+
+/**
  * @typedef {import('@/constants/order').ORDER_STATUS} OrderStatusValue
+ * @description 订单状态枚举值类型
  */
 
 /**
@@ -214,14 +268,25 @@ export default {
             trigger: 'blur'
           },
           {
+            /**
+             * 手机号码格式验证器
+             * @description 验证手机号码格式是否正确,支持1开头的11位数字
+             * @param {Object} rule - 验证规则对象
+             * @param {string} value - 待验证的值
+             * @param {Function} callback - 验证回调函数
+             * @returns {void}
+             */
             validator: (rule, value, callback) => {
-              if (!value) {
+              if (!value || typeof value !== 'string') {
                 callback()
                 return
               }
+              
+              // 手机号码正则表达式:1开头,第二位为3-9,总共11位数字
               const phoneRegex = /^1[3-9]\d{9}$/
-              if (!phoneRegex.test(value)) {
-                callback(new Error('请输入正确的手机号码'))
+              
+              if (!phoneRegex.test(value.trim())) {
+                callback(new Error('请输入正确的手机号码格式(1开头的11位数字)'))
               } else {
                 callback()
               }
@@ -299,69 +364,94 @@ export default {
 
     /**
      * 初始化表单数据
-     * @description 根据编辑模式初始化表单,编辑模式加载数据,新增模式重置表单
+     * @description 根据编辑模式初始化表单,编辑模式加载订单详情数据,新增模式重置表单为初始状态
      * @returns {Promise<void>}
+     * @throws {Error} 当初始化过程中发生错误时抛出异常
      * @public
      */
     async initForm() {
       try {
         if (this.isEdit && this.orderId) {
+          // 编辑模式:加载现有订单数据
           await this.loadOrderDetail(this.orderId)
         } else {
-          this.resetForm()
+          // 新增模式:重置表单为初始状态
+          await this.resetForm()
         }
       } catch (error) {
-        this.$message.error('初始化表单失败,请刷新页面重试')
+        console.error('初始化表单失败:', error)
+        const errorMessage = error.message || '初始化表单失败,请刷新页面重试'
+        this.$message.error(errorMessage)
+        throw error
       }
     },
 
     /**
      * 重置表单数据
-     * @description 将表单数据重置为初始状态,清除所有验证错误信息
+     * @description 将表单数据重置为初始状态,清除所有验证错误信息,并重置物料明细列表
      * @returns {Promise<void>}
+     * @throws {Error} 当重置过程中发生严重错误时抛出异常
      * @public
      */
     async resetForm() {
       try {
         // 重置表单数据为初始状态
         this.formData = this.createInitialFormData()
+        
+        // 重置物料明细列表(如果存在)
+        if (Array.isArray(this.materialDetails)) {
+          this.materialDetails = []
+        }
+        
+        // 重置保存状态
+        this.saveLoading = false
 
         // 等待DOM更新后清除表单验证
         await this.$nextTick()
 
+        // 清除表单验证状态
         if (this.$refs.orderForm && typeof this.$refs.orderForm.clearValidate === 'function') {
           this.$refs.orderForm.clearValidate()
         }
       } catch (error) {
-        // 重置表单时发生错误,静默处理
+        console.error('重置表单失败:', error)
+        // 重置表单时发生严重错误,抛出异常
+        throw new Error('重置表单失败,请刷新页面重试')
       }
     },
 
     /**
      * 加载订单详情数据
-     * @description 根据订单ID从服务器获取订单详情并填充到表单中,同时加载物料明细数据
-     * @param {string|number} orderId - 订单唯一标识
+     * @description 根据订单ID从服务器获取订单详情并填充到表单中,同时并行加载物料明细数据以提高性能
+     * @param {string|number} orderId - 订单唯一标识
      * @returns {Promise<void>}
-     * @throws {Error} 当API调用失败或数据格式错误时抛出异常
+     * @throws {Error} 当订单ID无效、API调用失败或数据格式错误时抛出异常
      * @public
      */
     async loadOrderDetail(orderId) {
-      if (!orderId) {
-        throw new Error('订单ID不能为空')
+      // 参数验证
+      if (!orderId || (typeof orderId !== 'string' && typeof orderId !== 'number')) {
+        throw new Error('订单ID不能为空且必须是有效的字符串或数字')
       }
 
       try {
-        // 并行加载订单详情和物料明细数据
+        // 并行加载订单详情和物料明细数据以提高性能
         /**
-         * @type {[AxiosResponse<ApiResponse<OrderItem>>, MaterialDetailRecord[]]}
+         * @type {[import('axios').AxiosResponse<ApiResponse<import('@/api/types/order').OrderRecord>>, MaterialDetailRecord[]]}
          */
         const [orderResponse, materialResponse] = await Promise.all([
           getDetail(orderId),
           this.loadMaterialDetails(orderId)
         ])
 
-        if (!orderResponse || !orderResponse.data || !orderResponse.data.data) {
-          throw new Error('服务器返回数据格式错误')
+        // 验证订单详情响应数据
+        if (!orderResponse?.data?.success) {
+          const errorMsg = orderResponse?.data?.msg || '获取订单详情失败'
+          throw new Error(errorMsg)
+        }
+
+        if (!orderResponse.data.data) {
+          throw new Error('订单数据不存在或已被删除')
         }
 
         const orderData = orderResponse.data.data
@@ -369,67 +459,107 @@ export default {
         // 安全地映射订单数据到表单,确保数据类型正确
         this.formData = this.mapOrderDataToForm(orderData)
 
-        // 设置物料明细数据
-        this.materialDetails = materialResponse
+        // 设置物料明细数据(确保是数组类型)
+        this.materialDetails = Array.isArray(materialResponse) ? materialResponse : []
+
+        console.log(`成功加载订单详情,订单编码: ${orderData.orderCode || orderId}`)
 
       } catch (error) {
-        const errorMessage = error.message || '加载订单详情失败'
-        this.$message.error(`${errorMessage},请重试`)
+        console.error('加载订单详情失败:', error)
+        const errorMessage = error.message || '加载订单详情失败,请检查网络连接后重试'
+        this.$message.error(errorMessage)
         throw error
       }
     },
 
     /**
-     * 加载订单物料明细数据
-     * @description 根据订单ID从服务器获取物料明细列表,远程数据不可删除
-     * @param {string|number} orderId - 订单唯一标识符
-     * @returns {Promise<MaterialDetailRecord[]>} 物料明细记录列表,失败时返回空数组
-     * @private
-     */
-    /**
      * 加载物料明细数据
-     * @description 根据订单ID获取物料明细列表,并为每个物料添加数据来源标识
-     * @param {string|number} orderId - 订单ID
-     * @returns {Promise<MaterialDetailRecord[]>} 处理后的物料明细数组
+     * @description 根据订单ID获取物料明细列表,并对数值字段进行格式化和验证,确保数据精确性和类型安全
+     * @param {string|number} orderId - 订单唯一标识符
+     * @returns {Promise<MaterialDetailRecord[]>} 格式化后的物料明细数组,数值字段已进行精度处理
+     * @throws {Error} 当订单ID无效或API调用失败时抛出异常
      * @private
      */
     async loadMaterialDetails(orderId) {
+      // 参数验证
+      if (!orderId || (typeof orderId !== 'string' && typeof orderId !== 'number')) {
+        console.error('loadMaterialDetails: 订单ID无效', orderId)
+        return []
+      }
+
       try {
         /**
-         * @type {AxiosResponse<ApiResponse<PageResult<OrderItemRecord>>>}
+         * @type {import('axios').AxiosResponse<ApiResponse<PageResult<OrderItemRecord>>>}
          */
         const response = await getOrderItemList(1, 1000, { orderId })
 
-        if (!response || !response.data || !response.data.data) {
-          throw new Error('物料明细数据格式错误')
+        // 验证响应数据结构
+        if (!response?.data?.success) {
+          const errorMsg = response?.data?.msg || '获取物料明细失败'
+          throw new Error(errorMsg)
+        }
+
+        if (!response.data.data) {
+          console.warn('物料明细数据为空')
+          return []
         }
 
-        const materialDetails = response.data.data.records || []
+        const materialDetails = response.data.data.records
+        
+        // 确保返回的是数组类型
+        if (!Array.isArray(materialDetails)) {
+          console.warn('物料明细数据格式异常,返回空数组')
+          return []
+        }
 
         // 为远程加载的物料数据添加数据来源标识并格式化数字字段
-        return materialDetails.map(material => {
-          // 验证和格式化数字字段
-          const orderQuantityValidation = validateNumber(material.orderQuantity)
-          const unitPriceValidation = validateNumber(material.unitPrice)
-          const taxRateValidation = validateNumber(material.taxRate)
-          const taxAmountValidation = validateNumber(material.taxAmount)
-          const totalAmountValidation = validateNumber(material.totalAmount)
-
-          return {
-            ...material,
-            dataSource: MaterialDetailDataSource.REMOTE,
-            isDeletable: false, // 远程加载的数据不可删除
-            // 格式化数字字段
-            orderQuantity: orderQuantityValidation.isValid ? Math.round(orderQuantityValidation.value) : 0,
-            unitPrice: unitPriceValidation.isValid ? preciseRound(unitPriceValidation.value, 2) : 0,
-            taxRate: taxRateValidation.isValid ? preciseRound(taxRateValidation.value, 4) : 0,
-            taxAmount: taxAmountValidation.isValid ? preciseRound(taxAmountValidation.value, 2) : 0,
-            totalAmount: totalAmountValidation.isValid ? preciseRound(totalAmountValidation.value, 2) : 0
+        return materialDetails.map((material, index) => {
+          try {
+            // 验证和格式化数字字段,确保类型安全
+            const orderQuantityValidation = validateNumber(material.orderQuantity)
+            const unitPriceValidation = validateNumber(material.unitPrice)
+            const taxRateValidation = validateNumber(material.taxRate)
+            const taxAmountValidation = validateNumber(material.taxAmount)
+            const totalAmountValidation = validateNumber(material.totalAmount)
+
+            return {
+              ...material,
+              dataSource: MaterialDetailDataSource.REMOTE,
+              isDeletable: false, // 远程加载的数据不可删除
+              // 格式化数字字段,确保精度和类型正确
+              orderQuantity: orderQuantityValidation.isValid ? Math.round(orderQuantityValidation.value) : 0,
+              unitPrice: unitPriceValidation.isValid ? preciseRound(unitPriceValidation.value, 2) : 0,
+              taxRate: taxRateValidation.isValid ? preciseRound(taxRateValidation.value, 4) : 0,
+              taxAmount: taxAmountValidation.isValid ? preciseRound(taxAmountValidation.value, 2) : 0,
+              totalAmount: totalAmountValidation.isValid ? preciseRound(totalAmountValidation.value, 2) : 0,
+              // 确保必要的字段存在
+              itemCode: material.itemCode || '',
+              itemName: material.itemName || '',
+              specs: material.specs || material.specification || '',
+              unit: material.unit || ''
+            }
+          } catch (itemError) {
+            console.error(`格式化物料明细第${index + 1}项失败:`, itemError)
+            // 返回默认的物料明细项,确保数据完整性
+            return {
+              ...material,
+              dataSource: MaterialDetailDataSource.REMOTE,
+              isDeletable: false,
+              orderQuantity: 0,
+              unitPrice: 0,
+              taxRate: 0,
+              taxAmount: 0,
+              totalAmount: 0,
+              itemCode: material.itemCode || '',
+              itemName: material.itemName || '',
+              specs: material.specs || material.specification || '',
+              unit: material.unit || ''
+            }
           }
         })
       } catch (error) {
-        this.$message.warning('加载物料明细失败,请稍后重试')
         console.error('加载物料明细失败:', error)
+        this.$message.warning('加载物料明细失败,请稍后重试')
         return []
       }
     },
@@ -452,6 +582,7 @@ export default {
         orderCode: String(orderData.orderCode || ''),
         orgCode: String(orderData.orgCode || ''),
         orgName: String(orderData.orgName || ''),
+        customerId: Number(orderData.customerId) || null,
         customerCode: String(orderData.customerCode || ''),
         customerName: String(orderData.customerName || ''),
         orderType: Number(orderData.orderType) || ORDER_TYPES.NORMAL,

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

@@ -37,7 +37,17 @@
         class="order-form"
         @submit="handleFormSubmit"
         @reset-change="handleFormReset"
-      />
+      >
+        <!-- 自定义客户选择组件 -->
+        <template #customerId="{ value, column }">
+          <customer-select
+            v-model="formData.customerId"
+            :placeholder="column.placeholder"
+            :disabled="column.disabled"
+            @customer-selected="handleCustomerSelected"
+          />
+        </template>
+      </avue-form>
 
       <!-- 物料明细区域 -->
       <div class="material-detail-section">
@@ -59,6 +69,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'
 
 /**
  * @typedef {import('./types').OrderFormModel} OrderFormModel
@@ -79,7 +90,8 @@ export default {
    * 组件注册
    */
   components: {
-    MaterialDetailTable
+    MaterialDetailTable,
+    CustomerSelect
   },
 
   /**
@@ -299,6 +311,24 @@ export default {
         this.$set(this.formData, 'totalQuantity', Math.round(totalQuantity))
         this.$set(this.formData, 'totalTaxAmount', Math.round(totalTaxAmount * 100) / 100)
       }
+    },
+
+    /**
+     * 处理客户选择事件
+     * @description 当客户选择组件选择客户时的回调处理,自动填充客户编码和客户名称
+     * @param {Object} customerData - 客户数据对象
+     * @param {string|number} customerData.customerId - 客户ID
+     * @param {string} customerData.customerCode - 客户编码
+     * @param {string} customerData.customerName - 客户名称
+     * @returns {void}
+     */
+    handleCustomerSelected(customerData) {
+      if (this.formData) {
+        // 更新客户相关字段
+        this.$set(this.formData, 'customerId', customerData.customerId)
+        this.$set(this.formData, 'customerCode', customerData.customerCode)
+        this.$set(this.formData, 'customerName', customerData.customerName)
+      }
     }
   }
 }